dA_Sidebar3

Track /watch count on all sites. See /watch counts in /watch menu and button

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         dA_Sidebar3
// @namespace    phi.pf-control.de/userscripts/dA_Sidebar3
// @version      1.8
// @description  Track /watch count on all sites. See /watch counts in /watch menu and button
// @author       Dediggefedde
// @match        *://*/*
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// @grant        GM.notification
// @license      MIT; http://opensource.org/licenses/MIT
// @noframes
// @sandbox      DOM
// ==/UserScript==


(function() {
	'use strict';

	//terminology note:
	//  - read/unread according to state on deviantart.
	//  - new/old according to script database

	let settings={
			checkInterval:60, //interval to check/make requests [seconds]
			quickCheck:2, //number of notification pages to check. 0= all
			countRead:true, //shows only unread notifications
			checkOnPageLoad:true, //checks dA whenever a new page is loaded
			hideBar:false, //move bar down when not hovered
			barPosition:0, //0 left, 1 center, 2 right
			theme:0,//0:green, 1:Dark, 2:Light, 3:Auto
			pulseNew:true, //red pulsing animation when entries are new
			dynLoad:true, //check notification pages only until known notifications appear
			showNotif:false, //show system notification on new messages
	};

	let messages=[]; //object {id, ts, cat, msg, unread, scrKnown}
	let lastCheck=0; //timestamp of last check (seconds since 1-1-1970 UTC)
	let lastNotifCnt=0; //number of new messages that the last system notification was displayed for.
	let CatMsgs=new Map(); // temp msg grouped by cat
	let scrKnown=new Set(); //old elements

	let token="expired"; //security token for requests
	let div,cont,setdiv,style; //sidebar, content container, settings dialog
	let pageCheckCounter; //counter for how many notification pages are left to check

	let imgGear = '<svg  xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 20.444057 20.232336" > <g transform="translate(-15.480352,-5.6695418)">  <g transform="matrix(0.26458333,0,0,0.26458333,25.702381,15.78571)"  style="fill:#000000">  <path  style="fill:#000000;stroke:#000000;stroke-width:1"  d="m 28.46196,-3.25861 4.23919,-0.48535 0.51123,0.00182 4.92206,1.5536 v 4.37708 l -4.92206,1.5536 -0.51123,0.00182 -4.23919,-0.48535 -1.40476,6.15466 4.02996,1.40204 0.45982,0.22345 3.76053,3.53535 -1.89914,3.94361 -5.1087,-0.73586 -0.4614,-0.22017 -3.60879,-2.2766 -3.93605,4.93565 3.02255,3.01173 0.31732,0.40083 1.8542,4.81687 -3.42214,2.72907 -4.2835,-2.87957 -0.32017,-0.39856 -2.26364,-3.61694 -5.68776,2.73908 1.41649,4.0249 0.11198,0.49883 -0.41938,5.14435 -4.26734,0.97399 -2.6099,-4.45294 -0.11554,-0.49801 -0.47013,-4.2409 h -6.31294 l -0.47013,4.2409 -0.11554,0.49801 -2.6099,4.45294 -4.26734,-0.97399 -0.41938,-5.14435 0.11198,-0.49883 1.41649,-4.0249 -5.68776,-2.73908 -2.26364,3.61694 -0.32017,0.39856 -4.2835,2.87957 -3.42214,-2.72907 1.8542,-4.81687 0.31732,-0.40083 3.02255,-3.01173 -3.93605,-4.93565 -3.60879,2.2766 -0.4614,0.22017 -5.1087,0.73586 -1.89914,-3.94361 3.76053,-3.53535 0.45982,-0.22345 4.02996,-1.40204 -1.40476,-6.15466 -4.23919,0.48535 -0.51123,-0.00182 -4.92206,-1.5536 v -4.37708 l 4.92206,-1.5536 0.51123,-0.00182 4.23919,0.48535 1.40476,-6.15466 -4.02996,-1.40204 -0.45982,-0.22345 -3.76053,-3.53535 1.89914,-3.94361 5.1087,0.73586 0.4614,0.22017 3.60879,2.2766 3.93605,-4.93565 -3.02255,-3.01173 -0.31732,-0.40083 -1.8542,-4.81687 3.42214,-2.72907 4.2835,2.87957 0.32017,0.39856 2.26364,3.61694 5.68776,-2.73908 -1.41649,-4.0249 -0.11198,-0.49883 0.41938,-5.14435 4.26734,-0.97399 2.6099,4.45294 0.11554,0.49801 0.47013,4.2409 h 6.31294 l 0.47013,-4.2409 0.11554,-0.49801 2.6099,-4.45294 4.26734,0.97399 0.41938,5.14435 -0.11198,0.49883 -1.41649,4.0249 5.68776,2.73908 2.26364,-3.61694 0.32017,-0.39856 4.2835,-2.87957 3.42214,2.72907 -1.8542,4.81687 -0.31732,0.40083 -3.02255,3.01173 3.93605,4.93565 3.60879,-2.2766 0.4614,-0.22017 5.1087,-0.73586 1.89914,3.94361 -3.76053,3.53535 -0.45982,0.22345 -4.02996,1.40204 z"  />  <circle  style="fill:#ffffff;stroke:#000000;stroke-width:1"  cx="0"  cy="0"  r="15" />  </g>  </g> </svg>';

	let themeNames=["","darktheme","lighttheme", "auto"];
	let autoTheme="";

	let iconMap=new Map([ //sidebar icon map, order of icons
			["Activity","🔔"],
			["Comments","📣"],
			["Replies","💬"],
			["Mentions","🚀"],
			["Correspondence","📬"],
	]);

	//Assigns categories to messages. Return values must be in iconMap! Return values are displayed as title.
	function assignCat(obj){
			//console.log("dA_Sidebar3:",obj) //uncomment to see message structures in the console
			if(obj.bucket=="bucket.mention")return "Mentions";
			else if(obj.type=="nc.comment")return "Comments";
			else if(obj.type=="nc.replied")return "Replies";
			else if(obj.messageClass.includes("correspondence"))return "Correspondence";
			else return "Activity";
	}

	//checks if a request need to be made or another website already triggered a request
	function makeRequest(){
			let ret=GM.getValue("lastCheck").then((time)=>{ //last request time in [s]
					if(Date.now()/1e3 - time < settings.checkInterval && !settings.checkOnPageLoad){
							GM.getValue("messages").then(msg=>{ //load notification from storage instead of deviantart
									messages=JSON.parse(msg);
									token="";
									return null;
							})
					}else{ //prepare request by loading security token
							return GM.getValue("token");
					}
			}).then(tok=>{ //csrf token. Needs to visit deviantart.com website to refresh
					if(tok==null)return null;
					token=tok;
					return request(); //web request to deviantart
			});
			return ret;
	}

	//requests notification pages from deviantart. Each response has a "cursor" hash to point to the next page
	function request(cursor=0){
			return new Promise(function(resolve, reject) {
					GM.xmlHttpRequest({
							method: "GET",
							url: `https://www.deviantart.com/_puppy/dashared/nc/bucket?bucket=bucket.user_feed_all&cursor=${cursor}&limit=20&csrf_token=${token}`, //puppy API request. limit=20 is maximum.
							headers: { //headers required for response
									"accept": 'application/json, text/plain, */*', //response in JSON format
									"content-type": 'application/json;charset=UTF-8'
							},
							onerror: function(response) {
									console.log("dA_Sidebar3:","error:", response);
									reject(response);
							},
							onload: async function(response) {
									let dat;
									try {
											dat = JSON.parse(response.responseText);
											cont.innerHTML=`Loading...(p${-pageCheckCounter})`; //display progress

											if(dat.status=="error" && dat?.errorDetails?.csrf){ //valid csrf token required
													token="expired"; //set it to invalid to show user error
													GM.setValue("token",token);
													updateHTML(); //display to user
													reject(dat); //cancel request
													return;
											}

											if(settings.dynLoad){ //dynamic loading: stop requesting new pages when a known notification is discovered
													for (const el of dat.messages) {
															if(messages.some(msg=>msg.id==el.messageId)){ //identify by messageId
																	resolve(dat);
																	return;
															}
													}
											}

											if (--pageCheckCounter!=0 && dat.hasMore) { //hasMore is true if another page exists. Unless user-defined max page-request is reached (pageCheckCounter==0)
													request(dat.cursor).then(nret => { //recursive call with next cursor/page
															dat.messages = dat.messages.concat(nret.messages); //callback: merge results
															resolve(dat);
															return;
													});
											} else { //if this is the last/only notification page to check
													resolve(dat);
											}
									} catch (e) {
											reject(e);
									}
							}
					});
			});
	}

	//initial call after storage is loaded (GM.getvalue)
	function init(){
			if(location.href.includes("deviantart.com")){ //fetch token when on deviantart.com and cancel
					let token=document.querySelector("input[name=validate_token]")?.value??token;
					GM.setValue("token",token);

					//fetch theme for auto-theme-mode
					if(document.body.classList.contains("theme-dark"))autoTheme="darktheme";
					else if(document.body.classList.contains("light-green"))autoTheme=""; //default
					else if(document.body.classList.contains("theme-light"))autoTheme="lighttheme";
					else autoTheme="";
					GM.setValue("autoTheme",autoTheme);
					// return;
			}

			injectHTML(); //inject sidebar into page

			if(settings.checkInterval>0 || settings.checkOnPageLoad)timer(); //request update (on pageload or initial)
			else updateHTML(); //update only manually: just display internal storage

			if(settings.checkInterval>0)setInterval(timer,settings.checkInterval*1e3); //request update in intervals
	}

	//generates text messages for notification objects. returns HTML code to be displayed as entry
	//note: contains only observed objects. there might be more, especially for commissions/group admins.
	//      will default to "Action of regarding {type}" and console.log the full object to be reported
	//note: Some objects not always contain the members, hence the object?.member??"" construct
	function convertMsgText(obj){
			try{
					let origNam=`<a class='dA_sb_user' target='_blank' href='https://www.deviantart.com/${obj.originator.username}'>${obj.originator.username}</a>`;
					let devTitl=(titl,url=null)=>`<${url==null?"span":"a target='_blank' href='"+url+"'"} class='dA_sb_title'>${titl}</${url==null?"span":"a"}>`;
					let comLink=(text,url=null)=>`<${url==null?"span":"a target='_blank' href='"+url+"'"} class='dA_sb_com'>${text}</${url==null?"span":"a"}>`;
					switch(obj.type){
							case "nc.fragments_replenish_receipt":
									return `You received ${devTitl(obj.messageData.fragmentsReplenishReceipt?.profit??"")} fragments.`;
							case "nc.fave":
									return `${origNam} added ${devTitl(obj.messageData.fave?.deviation?.title??"?",obj.messageData.fave?.deviation?.url)} to their favourites.`;
							case "nc.private_collect":
									return `${origNam}  added ${devTitl(obj.messageData.privateCollect?.deviation?.title??"?",obj.messageData.privateCollect?.deviation?.url)} to their private collection.`;
							case "nc.comment_liked":
									if(obj.messageData.comment?.comment?.commentable?.dataKey=="profile")return `${origNam} liked your comment their profile.`;
									else if(obj.messageData.comment?.comment?.commentable?.deviation) return `${origNam} liked your ${comLink("comment",obj.messageData.comment?.comment?.commentUrl)} on ${devTitl(obj.messageData.comment?.comment?.commentable?.deviation?.title??"?",obj.messageData.comment?.comment?.commentable?.deviation?.url)}.`;
									else if(obj.messageData.comment?.comment?.commentable?.forum) return `${origNam} liked your ${comLink("forum post",obj.messageData.comment?.comment?.commentUrl)} on ${devTitl(obj.messageData.comment?.comment?.commentable?.forum?.subject??"?","https://www.deviantart.com/forum/"+obj.messageData.comment?.comment?.commentable?.forum?.forumPath+"/"+obj.messageData.comment?.comment?.commentable?.forum?.threadId)}.`;
									else return `${origNam} liked your comment.`;
							case "nc.replied":
									if(obj.messageData.replied?.comment?.commentable?.dataKey=="profile")return `${origNam} replied to your comment on your profile.`;
									else if(obj.messageData.replied?.comment?.commentable?.deviation) return `${origNam} replied to your ${comLink("comment",obj.messageData.replied?.comment?.commentUrl)} on ${devTitl(obj.messageData.replied?.comment?.commentable?.deviation?.title??"?",obj.messageData.replied?.comment?.commentable?.deviation?.url)}.`;
									else if(obj.messageData.replied?.comment?.commentable?.forum) return `${origNam} replied to your ${comLink("forum post",obj.messageData.replied?.comment?.commentUrl)} on ${devTitl(obj.messageData.replied?.comment?.commentable?.forum?.subject??"?","https://www.deviantart.com/forum/"+obj.messageData.replied?.comment?.commentable?.forum?.forumPath+"/"+obj.messageData.replied?.comment?.commentable?.forum?.threadId)}.`;
									else return `${origNam} replied to your comment.`;
							case "nc.badge_given":
									return `${origNam}  gave you a ${devTitl(obj.messageData.badgeGiven?.badge?.title??"")} badge.`;
							case "nc.badge_levelled":
									return `${origNam} levelled up your ${devTitl(obj.messageData.badgeLevelled?.badge?.baseTitle??"")} badge to ${devTitl(obj.messageData.badgeLevelled?.badge?.title??"")}.`;
							case "nc.group_join_request_receipt":
									return `Your group membership in ${origNam} is currently on vote.`;
							case "nc.comment_mentions_deviation":
									return `${origNam} ${comLink("mentioned",obj.messageData.commentMentionsDeviation?.mentioner?.commentUrl)} your deviation ${devTitl(obj.messageData.commentMentionsDeviation?.mentioned?.title??"?",obj.messageData.commentMentionsDeviation?.mentioned?.url)}.`;
							case "nc.comment":
									return `${origNam} ${comLink("commented",obj.messageData.comment?.comment?.commentUrl)} on ${devTitl(obj.messageData.comment?.comment?.commentable?.deviation?.title??"your site",obj.messageData.comment?.comment?.commentable?.deviation?.url)}.`;
							case "nc.collect":
									return `${origNam} added your work ${devTitl(obj.messageData.collect?.deviation?.title??"?",obj.messageData.collect?.deviation?.url)} to their collection.`;
							case "nc.deviation_mentions_deviation":
									return `${origNam} ${comLink("mentioned",obj.messageData.deviationMentionsDeviation?.mentioner?.commentUrl)} your work ${devTitl(obj.messageData.deviationMentionsDeviation?.mentioned?.title??"?",obj.messageData.deviationMentionsDeviation?.mentioned?.url)} to their collection.`;
							case "nc.new_watcher":
									return `${origNam} is now watching you!`;
							case "nc.deviation_submission_offer_artist_receipt":
									return `${origNam} accepted your group submission ${devTitl(obj.messageData.correspondence?.bppModule?.groupDeviation?.deviation?.title??"",obj.messageData.correspondence?.bppModule?.groupDeviation?.deviation?.url)}`;
							case "nc.blog_submission_author_receipt":
									return `${origNam} posted a new blog${devTitl(obj.messageData.correspondence?.bppModule?.groupBlog?.deviation?.title??"?",obj.messageData.correspondence?.bppModule?.groupBlog?.deviation?.url)}!`;
							case "nc.radom_recommendation":
									return `Please welcome the new user ${origNam}!`;
							case "nc.group_created":
									return `Group ${origNam} created!`;
							case "nc.award_badge_given_on_deviation":
									return `${origNam} gave you a ${devTitl(obj.messageData.awardBadgeGivenOnDeviation?.badge?.title)??""} badge for your work ${devTitl(obj.messageData.awardBadgeGivenOnDeviation?.deviation?.title,obj.messageData.awardBadgeGivenOnDeviation?.deviation?.url)??""}.`;
							case "nc.award_badge_given_on_comment":
									return `${origNam} gave you a ${devTitl(obj.messageData.awardBadgeGivenOnComment?.badge?.title)??""} badge for your your ${comLink("comment",obj.messageData.awardBadgeGivenOnComment?.comment?.commentUrl)}!`;
							default:
									if(obj.originator.username){
											return `Action of ${origNam}  regarding ${obj.type}`;
									} else{
											return `Action of regarding ${obj.type}`;
									}
					}
			}catch(ex){
					console.log("dA_Sidebar3 unknown:",ex,obj.type,JSON.stringify(obj));
					return `Action of regarding ${obj.type}`;
			}
	}

	//update request timer
	function timer(){

			// makeRequest: requests {settings.quickCheck} pages or load from storage if last request was within {checkInterval} from another page
			pageCheckCounter=settings.quickCheck;
			makeRequest().then(ret=>{
					if(ret==null){ //invalid csrf or already checked.
							updateHTML(); //update UI
							return;
					}

					lastCheck=Date.now()/1e3;

					if(settings.dynLoad){//dynamic load: remove old notifications with timestamp newer than the oldest message in dynamic loading
							let minTS=ret.messages.reduce(function(a, b) {return (a.ts < b.ts) ? a.ts : b.ts},"9999-08-05T12:29:21.000Z"); //minimum timestamp
							messages=messages.filter(el=>el.ts<minTS);
					}else{ //without dynamic loading: discard old storage
							messages=[];
					}

					ret.messages.forEach(el=>{ //add new notifications to messages-array, identified by messageId
							messages.push({ //messages should not be in both arrays at this point. old present messages at dyn loading will be removed by filter previously
									id:el.messageId,
									ts:el.ts, //timestamp
									cat:assignCat(el), //assign category by notification type
									msg:convertMsgText(el), //generate notification text message
									unread:el.isNew //read/unread from deviantart. old/new terminology in script highlight
							});
					});
					messages=messages.sort((a,b)=>a.ts<b.ts); //sort notifications by timestamp

					//console.log(ret.messages);

					GM.setValue("messages",JSON.stringify(messages)); //update storage
					GM.setValue("lastCheck",lastCheck);

					updateHTML();//refresh UI
			}).catch(ret=>{console.log("dA_Sidebar3:","An error occured:",ret)});
	}

	function strip(html){
			let doc = new DOMParser().parseFromString(html, 'text/html');
			return doc.body.textContent || "";
	}
	//highlight or normalize sidebar, check for notifications being new
	function highlight(reset=false){ //reset = mark all as known

			//restore default
			div.classList.remove("dA_Sidebar3_newBar");
			document.querySelectorAll(".dA_Sidebar3_newEntr").forEach(el=>el.classList.remove("dA_Sidebar3_newEntr"));
			document.querySelectorAll(".dA_sidebar3_entr_hot").forEach(el=>el.classList.remove("dA_sidebar3_entr_hot"));

			if(reset){
					scrKnown=new Set(messages.map(el=>el.id));// all are known, remove unused message ids
					lastNotifCnt=0; //rest system notification counter
					GM.setValue("lastNotifCnt",lastNotifCnt); //update storage
					GM.setValue("messages",JSON.stringify(messages));
					GM.setValue("scrKnown",JSON.stringify([...scrKnown]));
					updateHTML();//update UI
					return;
			}

			let cntNew=0; //counter of new notifications in script

			messages.forEach(val=>{ //count new notification & highlight counter in sidebar
					if(settings.countRead && !val.unread)return; //if set, only highlight on unread messages
					if(scrKnown.has(val.id))return // old news in script storage
							++cntNew;
					document.querySelector("#dA_Sidebar3 span[title='"+val.cat+"']").classList.add("dA_Sidebar3_newEntr");
			});

			if(cntNew>0){ //highlight sidebar and show system notification
					div.classList.add("dA_Sidebar3_newBar")

					if(settings.showNotif){
							GM.getValue("lastNotifCnt",0).then(ret=>{ //show notification only if not shown already for this amount of new notifications
									let detailmsg="\n"+strip(cont.innerHTML)+"\n"+strip(messages[0].msg);

									// console.log(ret,cntNew,lastNotifCnt);
									if(ret<cntNew){
											GM.notification({ title: "dA_Sidebar3",text: cntNew+" new DeviantArt notifications"+detailmsg, url:"https://deviantart.com/notifications" });
									}
									lastNotifCnt=cntNew; //update counter for shown system notifications
									GM.setValue("lastNotifCnt",lastNotifCnt);
							});
					}
			}

	}

	//opens the setting dialog and shows present settings
	function showSettings(){
			setdiv.style.display="block"; //show form

			//settings loaded at pageload

			//load settings
			let form=document.forms.dA_Sidebar3_form;
			form.elements.checkInterval.value=settings.checkInterval;
			form.elements.checkInterval.removeAttribute("readonly");
			if(settings.checkInterval==0){
					form.elements.checkInterval.value=0;
					form.elements.checkInterval.setAttribute("readonly","");
					form.elements.checkIntervalNot.checked=true;
			}

			form.elements.quickCheck.value=settings.quickCheck;
			form.elements.quickCheck.removeAttribute("readonly");
			if(settings.quickCheck==0){
					form.elements.quickCheck.value=0;
					form.elements.quickCheck.setAttribute("readonly","");
					form.elements.quickCheckAll.checked=true;
			}
			form.elements.countNew.checked=settings.countRead;
			form.elements.checkOnPageLoad.checked=settings.checkOnPageLoad;
			form.elements.hideBar.checked=settings.hideBar;
			form.elements.barPosition[settings.barPosition].checked=true;
			form.elements.theme[settings.theme].checked=true;
			form.elements.pulseNew.checked=settings.pulseNew;
			form.elements.dynLoad.checked=settings.dynLoad;
			form.elements.showNotif.checked=settings.showNotif;
	}

	//close setting dialog and save chosen settings
	function saveSettings(){
			setdiv.style.display="none"; //close dialog

			//save chosen settings
			let form=document.forms.dA_Sidebar3_form
			settings.checkInterval = parseInt(form.elements.checkInterval.value);
			if(form.elements.checkIntervalNot.checked)settings.checkInterval=0;
			else if(settings.checkInterval<10)settings.checkInterval=10;

			settings.quickCheck = form.elements.quickCheck.value;
			if(form.elements.quickCheckAll.checked)settings.quickCheck=0;

			settings.countRead = form.elements.countNew.checked;
			settings.checkOnPageLoad = form.elements.checkOnPageLoad.checked;
			settings.hideBar = form.elements.hideBar.checked;
			settings.pulseNew = form.elements.pulseNew.checked;
			settings.dynLoad= form.elements.dynLoad.checked;
			settings.showNotif = form.elements.showNotif.checked;

			document.forms.dA_Sidebar3_form.elements.barPosition.forEach((el,ind)=>{if(el.checked)settings.barPosition=ind});
			document.forms.dA_Sidebar3_form.elements.theme.forEach((el,ind)=>{if(el.checked)settings.theme=ind});


			//store settings in storage
			GM.setValue("settings",JSON.stringify(settings));

			updateHTML();
			insertStyle(); //update view
	}

	//helper: parse as int, even NaN, and return at least minimum
	function cropmin(val,min){
			let intval=parseInt(val);
			if(isNaN(intval)||intval<min)return min;
			return intval;
	}

	function colorTime(){ //assign CSS classes to notification entries depending on their timestamp
			let n=new Date();
			[...document.querySelectorAll("#dA_Sidebar3_popup span.dA_sidebar3_entr_tim")].forEach(el=>{
					let diff=(n-(new Date(el.getAttribute("ts"))))/60e3; //time difference in [minutes]

					if(diff<10)el.classList.add("dA_sb2_10min");
					else if(diff<60)el.classList.add("dA_sb2_1h");
					else if(diff<300)el.classList.add("dA_sb2_5h");
					else if(diff<1440)el.classList.add("dA_sb2_1d");
					else if(diff<7200)el.classList.add("dA_sb2_5d");
			});

	}

	//inject sidebar HTML and event handlers. Calls to insert setting dialog and insert style.
	function injectHTML(){
			div =document.createElement("div"); //main sidebar div
			div.id="dA_Sidebar3";

			cont =document.createElement("div"); //main content div to change via innerHTML=""
			cont.innerHTML=`Loading...`; //initial content while loading storage
			cont.addEventListener("click",()=>{ //click removes highlight
					highlight(true);
			},false)
			div.append(cont);

			let setBut =document.createElement("button"); //setting button
			setBut.innerHTML=imgGear;
			setBut.id="dA_Sidebar3_setButton";
			setBut.addEventListener("click",showSettings,false);
			div.append(setBut);

			let popupdiv=document.createElement("div"); //popup for notification texts
			popupdiv.id="dA_Sidebar3_popup";
			popupdiv.innerHTML="nothing...";
			div.append(popupdiv);
			document.body.append(div);

			//event handlers
			div.addEventListener("mouseleave",()=>{ //hide popup when leaving sidebar
					let els=document.getElementById("dA_Sidebar3_popup");
					els.innerHTML="";
			},false);
			popupdiv.addEventListener("click",(ev)=>{ //click removes highlight
					if(ev.target.tagName!="A")highlight(true);
			},false)
			div.addEventListener("mouseover",(ev)=>{ //show popup with notification texts when hovering over category
					let els=document.getElementById("dA_Sidebar3_popup");
					if(ev.target.title && CatMsgs.has(ev.target.title)){ //load only pre-generated text
							els.innerHTML=CatMsgs.get(ev.target.title); //updated in updateHTML
							els.style.height="auto";
							els.style.bottom=div.clientHeight+"px";
							colorTime(); //color according to timestamp
					}
			},false);

			insertSettingform(); //add setting form
			insertStyle(); //add CSS style
	}

	//insert setting form HTML and event handlers
	function insertSettingform(){

			//HTML for setting form. Initially unset and hidden. Present settings are loaded with showSettings()
			let settmp=`
		<form id='dA_Sidebar3_form'>
		<label for="checkInterval" title='min. 10 s'>
			<span>Update interval [s]</span>
			<input type="text" id="checkInterval" placeholder="min. 10 s" style='width:20%;'/>
								<label for="checkIntervalNot" style='width:24%;'>
				<input type="checkbox" id="checkIntervalNot" placeholder="0 = all"/>
									<span style='margin:0!important;'>Manual</span>
								</label>
		</label>
		<label for="quickCheck" title='Checks the latest 20 Notification per page.'>
			<span>Requested notification pages</span>
			<input type="text" id="quickCheck" placeholder="# of pages" style='width:20%;'/>
								<label for="quickCheckAll" style='width:24%;'>
				<input type="checkbox" id="quickCheckAll" placeholder="0 = all"/>
									<span style='margin:0!important;'>All</span>
								</label>
		</label>
		<label for="dynLoad" title='Only requests notification pages until a known message-ID is found'>
			<span>Dynamic request limits</span>
			<input type="checkbox" id="dynLoad"/>
		</label>
		<label for="countNew" title='0 = all'>
			<span>Show only unread notifications</span>
			<input type="checkbox" id="countNew"/>
		</label>
		<label for="checkOnPageLoad" title='Requests an update whenever a new page is visited'>
			<span>Update on pageload</span>
			<input type="checkbox" id="checkOnPageLoad"/>
		</label>
		<label for="hideBar" title='Hides notification bar except 2px. Hover there to show the bar again. '>
			<span>Hide sidebar</span>
			<input type="checkbox" id="hideBar"/>
		</label>
		<label for="pulseNew" title='Plays a pulse animation when new notifications appear. Click the bar to mark them as read.'>
			<span>Pulse animation on new notification</span>
			<input type="checkbox" id="pulseNew"/>
		</label>
		<label for="showNotif" title='Shows a system notification for new messages, if allowed in your browser settings.'>
				<span>Show system notifications</span>
				<input type="checkbox" id="showNotif"/>
		</label>
		<label title='Alignment of sidebar at the bottom of the window.'>
			<span>SideBar position</span>
			<label for='barPositionL'><input type="radio" id="barPositionL" name='barPosition'/><span>Left</span></label>
			<label for='barPositionC'><input type="radio" id="barPositionC" name='barPosition'/><span>Center</span></label>
			<label for='barPositionR'><input type="radio" id="barPositionR" name='barPosition'/><span>Right</span></label>
		</label>
		<label title='Choose a theme for the sidebar.'>
			<span style='width: 120px;'>Theme</span>
			<label for='themeGreen'><input type="radio" id="themeGreen" name='theme'/><span>Green</span></label>
			<label for='themeLight'><input type="radio" id="themeLight" name='theme'/><span>Dark</span></label>
			<label for='themeDark'><input type="radio" id="themeDark" name='theme'/><span>Light</span></label>
			<label for='themeAuto'><input type="radio" id="themeAuto" name='theme'/><span>Auto (dA)</span></label>
		</label>
		</form>
		<button type="button" id='dA_Sidebar3_saveset'>Save</button>
		<button type="button" id='dA_Sidebar3_cancelset'>Cancel</button>
		`;

			setdiv=document.createElement("div"); //setting form
			setdiv.innerHTML=settmp;
			setdiv.id="dA_Sidebar3_settings";
			document.body.append(setdiv);

			//event handlers
			document.getElementById("checkInterval").addEventListener("focusout",(ev)=>{ev.target.value=cropmin(ev.target.value,10);},false);//minValue checkInterval 10 [s]
			document.getElementById("quickCheck").addEventListener("focusout",(ev)=>{ev.target.value=cropmin(ev.target.value,0);},false);//minValue quickCheck 0 pages

			//save/cancel buttons
			document.getElementById("dA_Sidebar3_saveset").addEventListener("click",saveSettings,false);
			document.getElementById("dA_Sidebar3_cancelset").addEventListener("click",()=>{setdiv.style.display="none";},false);

			//checkmarks Check all pages. enable/disable text input
			document.getElementById("quickCheckAll").addEventListener("click",(ev)=>{
					if(ev.target.checked) document.getElementById("quickCheck").setAttribute("readonly","");
					else {document.getElementById("quickCheck").removeAttribute("readonly");document.getElementById("quickCheck").value=2;}
			},false);
			//checkmarks only manual. enable/disable text input
			document.getElementById("checkIntervalNot").addEventListener("click",(ev)=>{
					if(ev.target.checked) document.getElementById("checkInterval").setAttribute("readonly","");
					else{ document.getElementById("checkInterval").removeAttribute("readonly");document.getElementById("checkInterval").value=60;}
			},false);
	}

	//CSS style, add as <style> in <head> or <body> if website is headless
	function insertStyle(){
			let styleText=`
				/*default style: Greentheme*/
				#dA_Sidebar3 {user-select:none;position: fixed;bottom: 0;min-width:300px;width:auto;max-width: 400px;height: auto;border: 1px solid black;
					${settings.barPosition==1?"left:50%;":settings.barPosition==2?"right:0;":"left: 0;"}
					border-top-right-radius: 5px;font-family: Georgia;font-size: 12pt;line-height: 16pt;color: black;
					background: linear-gradient(#cbf9b9,#7fc458);padding: 3px;padding-right:20px;z-index:7777777;
					box-sizing: content-box;${settings.hideBar?"transform:translateY(100%) translateY(-5px)"+(settings.barPosition==1?" translateX(-50%);":";"):settings.barPosition==1?"transform:translateX(-50%);":""}}
				#dA_Sidebar3:hover{${settings.barPosition==1?"transform:translateX(-50%);":"transform:none;"}}
				#dA_Sidebar3.dA_Sidebar3_newBar{border:1px solid red;${settings.pulseNew?"animation: dA_Sidebar3_pulse 1s ease-out infinite":""};}
				#dA_Sidebar3 span.dA_Sidebar3_newEntr{color:red;}
				#dA_Sidebar3 *{margin:0;padding:0;}
				#dA_Sidebar3 img {vertical-align: middle;height: 1.4em; display: inline-block;}
				#dA_Sidebar3 a {cursor:pointer;color:black;text-decoration:underline;}
				#dA_Sidebar3>div>span {margin: 0 5px;cursor:help;white-space: nowrap;}
				#dA_Sidebar3 button{position: absolute;line-height: 16pt!important;background: none;border: none;cursor: pointer;}
				#dA_Sidebar3 button:hover{filter: invert(10%) sepia(100%) saturate(5000%) hue-rotate(359deg) brightness(150%);}
				#dA_Sidebar3_setButton{top: 1px;right: 1px;width:20px;height:20px;}
				#dA_Sidebar3_closeButton{top: -4px;right: 20px;width: 12px;height: 20px;font-size: 17px;}
				#dA_Sidebar3_setButton svg{vertical-align:top;}
				@keyframes dA_Sidebar3_pulse {
						0%   { box-shadow: 0 0 0 red; }
						50%  { box-shadow: 0 0 17px red; }
						100% { box-shadow: 0 0 0 red; }
				}
				#dA_Sidebar3_settings {display:none;user-select:none;width:450px;position:fixed;z-index:777777;border-radius:15px;border:1px solid black;box-shadow: 2px 2px 2px black;left:50%;top:50%;transform:translate(-50%,-50%);background-color:#90ca90;}
				#dA_Sidebar3_settings * {vertical-align:middle;}
							#dA_Sidebar3_settings input[readonly]{background-color:#ccc;}
				#dA_Sidebar3_settings, #dA_Sidebar3_settings span, #dA_Sidebar3_settings div, #dA_Sidebar3_settings label{font: 12pt Georgia normal normal normal!important;line-height: 16pt!important;color: black!important;padding:0!important;margin:0!important;}
				#dA_Sidebar3_settings form > label > span {width: 210px;  display: inline-block!important;}
				#dA_Sidebar3_settings label{padding: 5px 0!important;cursor:help!important;display:inline-block;}
				#dA_Sidebar3_settings form{display:grid;padding: 10px!important;margin-bottom:40px;}
				#dA_Sidebar3_settings input[type="text"] {background:white;box-shadow: 0px 0px 1px 1px #84a884 inset; appearance: textfield;   opacity: 1;box-sizing: content-box;  width: 180px;  height:20px; font: 12pt georgia normal normal normal !important; padding: 2px; margin: 0;border:1px solid grey;  border-radius: 5px;}
					#dA_Sidebar3_settings input[type='checkbox']{cursor:pointer;  width: 40%;  height: 20px;margin:0; appearance: checkbox;  opacity: 1;}
					#dA_Sidebar3_settings input[type='radio']{cursor:pointer;  width: 15px;  height: 15px;margin:0;vertical-align:middle;  appearance: radio;  opacity: 1;}
					#dA_Sidebar3_settings label span{margin: 0 5px!important;  opacity: 1;}
				#dA_Sidebar3_settings form>label { border-bottom: 1px dashed gray;}
				#dA_Sidebar3_settings button{font:12pt Georgia normal normal normal !important;position:absolute;bottom:10px;transform:translateX(-50%);padding: 5px 20px;box-shadow: 1px 1px;cursor: pointer;  border-radius: 5px;color: black;}
				#dA_Sidebar3_saveset {left:33%;background: linear-gradient(#c7e8a5, #99d01f);}
				#dA_Sidebar3_settings button:hover{  filter: brightness(110%);}
				#dA_Sidebar3_settings button:active{  filter: brightness(90%);box-shadow: 1px 1px inset;}
				#dA_Sidebar3_cancelset {left:66%;background:linear-gradient(#ffe3e3, #fd9c91)}
				#dA_Sidebar3_popup {position:absolute;bottom:30px;height:0;width:100%;left:0;background:linear-gradient(#cbf9b9,#7fc458);overflow:clip;overflow-y:auto;max-height:300px;}
				#dA_Sidebar3_popup>div {margin: 0px;  padding: 5px;  border-bottom: 1px dashed black;position:relative;}
				#dA_Sidebar3_popup .dA_sb_title{color:rgb(234, 52, 47);}
				#dA_Sidebar3_popup .dA_sb_user{color:blue;}
				#dA_Sidebar3_popup .dA_sb_com{color:darkgreen;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim{position:absolute;top:-7px;right:0;font-size:7pt;color:black;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_hot{background-color:#ff000044}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_10min{color:#f00;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1h{color:#d00;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5h{color:#a00;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1d{color:#900;}
				#dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5d{color:#700;}

				/* dA style dark*/
				#dA_Sidebar3.darktheme {background: linear-gradient(#000,#222);border-radius:1px;color:white;}
				#dA_Sidebar3.darktheme a{color:white;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_setButton{filter:invert();}
				#dA_Sidebar3.darktheme #dA_Sidebar3_setButton:hover{filter: invert(10%) sepia(100%) saturate(5000%) hue-rotate(359deg) brightness(150%);}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup {background:linear-gradient(#000,#111);}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup>div {border-color: white;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sb_title{color:#f77;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sb_user{color:#77f;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sb_com{color:#7f7;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim{color:#fff;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_hot{background-color:#600}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_10min{color:#f00;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1h{color:#f33;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5h{color:#f77;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1d{color:#faa;}
				#dA_Sidebar3.darktheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5d{color:#fcc;}
				#dA_Sidebar3_settings.darktheme {border-radius:1px;border: 1px solid #777;box-shadow: none;background-color:#222;}
				#dA_Sidebar3_settings.darktheme div, #dA_Sidebar3_settings.darktheme label, #dA_Sidebar3_settings.darktheme span {color:#ddd!important;}
				#dA_Sidebar3_settings.darktheme #dA_Sidebar3_saveset{background:linear-gradient(to right, #01f2fc,#01fe93);}
				#dA_Sidebar3_settings.darktheme #dA_Sidebar3_cancelset{background:#f53948;}
				#dA_Sidebar3_settings.darktheme button{border-radius:0;border:none;}


				/* dA style light*/
				#dA_Sidebar3.lighttheme {background: linear-gradient(#fff,#eee);border-radius:1px;color:#000;}
				#dA_Sidebar3.lighttheme a{color:#222;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup {background:linear-gradient(#fff,#eee);}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup>div {border-color: black;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sb_title{color:#a00;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sb_user{color:#00a;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sb_com{color:#0a0;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim{color:#000;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_hot{background-color:#fcc}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_10min{color:#f00;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1h{color:#d00;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5h{color:#a00;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1d{color:#900;}
				#dA_Sidebar3.lighttheme #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5d{color:#700;}
				#dA_Sidebar3_settings.lighttheme {border-radius:1px;border: 1px solid #aaa;box-shadow: none;background-color:#fff;}
				#dA_Sidebar3_settings.lighttheme div, #dA_Sidebar3_settings.lighttheme  label, #dA_Sidebar3_settings.lighttheme  span {color:#333!important;}
				#dA_Sidebar3_settings.lighttheme #dA_Sidebar3_saveset{background:linear-gradient(to right, #01f2fc,#01fe93);}
				#dA_Sidebar3_settings.lighttheme #dA_Sidebar3_cancelset{background:#f53948;}
				#dA_Sidebar3_settings.lighttheme button{border-radius:0;border:none;}
				`;


			if(style==null){
					style=document.createElement('style');
					style.id='dA_Sidebar3_style';
					let head=document.getElementsByTagName('head')[0];
					if(!head)document.body.appendChild(style);
					else document.head.appendChild(style);
			}

			style.innerHTML=styleText;
	}

	//update UI and generate notification text messages for hover
	function updateHTML(){

			let curTheme=themeNames[settings.theme??0];
			if(curTheme=="auto")curTheme=autoTheme;
			div.className=curTheme;
			setdiv.className=curTheme;

			if(token=="expired"){ //csrf token invalid or initial call
					cont.innerHTML="CSRF Expired! Refresh authentification by visiting <a href='https://deviantart.com' target='_blank'>deviantart.com</a>.";
			}else{
					let cats=new Map(); //counter for new messages per category {cat:#new}
					CatMsgs=new Map(); // popup text messages for each category {cat:HTML-list}
					let sum=0; //total amount of notifications

					messages.forEach((val)=>{ //count notifications for each category in {cats}, total in {sum} and generate popup text in {CatMsgs}
							if(settings.countRead && !val.unread)return; //ignore not new if setting is set
							cats.set(val.cat,(cats.get(val.cat)??0)+1); //increment Map element for category
							sum+=1;
							let tim=/(.*?)T(.*?)-.*/.exec(val.ts) //parse timestamp. It's UTC-7.
							let dtim=new Date(`${tim[1]} ${tim[2]} UTC-7`)

							CatMsgs.set(val.cat, //concat all notificiation texts in {val.msg} for each category and add timestamp display <span>
													`${CatMsgs.get(val.cat)??""}
						<div ${!scrKnown.has(val.id)?"class='dA_sidebar3_entr_hot'":""}>${val.msg}
						<span class='dA_sidebar3_entr_tim' ts='${val.ts}'>${dtim.toLocaleString()}
						</span></div>`
												 );
					});
					//display <span> with title, text and notification count for each category in iconMap. returns HTML code
					let mapstr= [...iconMap].reduce((acc,[key,val])=>{
							return `${acc}<span title=${key}>${val??key} ${cats.get(key)??0}</span>`
					},"")
					cont.innerHTML=`New (<a target="_blank" href="https://www.deviantart.com/notifications">${sum}</a>): ${mapstr}`; //content of sidebar. categories preceeded with New(#) with total sum and link to /notifications

					highlight();//check if there are new notifications and highlight
			}
	}

	//initial entry: load storage
	Promise.all([
			GM.getValue("settings",JSON.stringify(settings)),
			GM.getValue("messages",JSON.stringify(messages)),
			GM.getValue("lastCheck",lastCheck),
			GM.getValue("scrKnown","[]"),
			GM.getValue("autoTheme","")
	]).then(res=>{ //only proceed if all is loaded
			let tmp=JSON.parse(res[0]); //settings
			Object.entries(tmp).forEach(([key,val])=>{settings[key]=val;}); //load old settings, keep unset ones

			messages=JSON.parse(res[1]); //internal notification storage

			lastCheck=res[2];//timestamp of last update request
			if(settings.checkOnPageLoad)lastCheck=0; //reset timestemp to load immediately

			scrKnown=new Set(JSON.parse(res[3])); //list of old ids where no notification/highlight is sent for

			autoTheme=res[4];

			init(); //entry function: insert HTML, Css and start timer.
	});

})();