dA_Sidebar3

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

当前为 2024-08-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name dA_Sidebar3
  3. // @namespace phi.pf-control.de/userscripts/dA_Sidebar3
  4. // @version 1.3
  5. // @description Track /watch count on all sites. See /watch counts in /watch menu and button
  6. // @author Dediggefedde
  7. // @match *://*/*
  8. // @grant GM.xmlHttpRequest
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // @grant GM.addStyle
  12. // @grant GM.notification
  13. // @license MIT; http://opensource.org/licenses/MIT
  14. // @noframes
  15. // @sandbox DOM
  16. // ==/UserScript==
  17.  
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. //terminology note:
  23. // - read/unread according to state on deviantart.
  24. // - new/old according to script database
  25.  
  26. let settings={
  27. checkInterval:60, //interval to check/make requests [seconds]
  28. quickCheck:2, //number of notification pages to check. 0= all
  29. countRead:true, //shows only unread notifications
  30. checkOnPageLoad:true, //checks dA whenever a new page is loaded
  31. hideBar:true, //move bar down when not hovered
  32. barPosition:0, //0 left, 1 center, 2 right
  33. pulseNew:true, //red pulsing animation when entries are new
  34. dynLoad:true, //check notification pages only until known notifications appear
  35. showNotif:false, //show system notification on new messages
  36. };
  37.  
  38. let messages=[]; //object {id, ts, cat, msg, unread, scrKnown}
  39. let lastCheck=0; //timestamp of last check (seconds since 1-1-1970 UTC)
  40. let lastNotifCnt=0; //number of new messages that the last system notification was displayed for.
  41. let CatMsgs=new Map(); // temp msg grouped by cat
  42. let scrKnown=new Set(); //old elements
  43.  
  44. let token="expired"; //security token for requests
  45. let div,cont,setdiv,style; //sidebar, content container, settings dialog
  46. let pageCheckCounter; //counter for how many notification pages are left to check
  47.  
  48. 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>';
  49.  
  50. let iconMap=new Map([ //sidebar icon map, order of icons
  51. ["Activity","🔔"],
  52. ["Comments","📣"],
  53. ["Replies","💬"],
  54. ["Mentions","🚀"],
  55. ["Correspondence","📬"],
  56. ]);
  57.  
  58. //Assigns categories to messages. Return values must be in iconMap! Return values are displayed as title.
  59. function assignCat(obj){
  60. //console.log("dA_Sidebar3:",obj) //uncomment to see message structures in the console
  61. if(obj.bucket=="bucket.mention")return "Mentions";
  62. else if(obj.type=="nc.comment")return "Comments";
  63. else if(obj.type=="nc.replied")return "Replies";
  64. else if(obj.messageClass.includes("correspondence"))return "Correspondence";
  65. else return "Activity";
  66. }
  67.  
  68. //checks if a request need to be made or another website already triggered a request
  69. function makeRequest(){
  70. let ret=GM.getValue("lastCheck").then((time)=>{ //last request time in [s]
  71. if(Date.now()/1e3 - time < settings.checkInterval && !settings.checkOnPageLoad){
  72. GM.getValue("messages").then(msg=>{ //load notification from storage instead of deviantart
  73. messages=JSON.parse(msg);
  74. token="";
  75. return null;
  76. })
  77. }else{ //prepare request by loading security token
  78. return GM.getValue("token");
  79. }
  80. }).then(tok=>{ //csrf token. Needs to visit deviantart.com website to refresh
  81. if(tok==null)return null;
  82. token=tok;
  83. return request(); //web request to deviantart
  84. });
  85. return ret;
  86. }
  87.  
  88. //requests notification pages from deviantart. Each response has a "cursor" hash to point to the next page
  89. function request(cursor=0){
  90. return new Promise(function(resolve, reject) {
  91. GM.xmlHttpRequest({
  92. method: "GET",
  93. 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.
  94. headers: { //headers required for response
  95. "accept": 'application/json, text/plain, */*', //response in JSON format
  96. "content-type": 'application/json;charset=UTF-8'
  97. },
  98. onerror: function(response) {
  99. console.log("dA_Sidebar3:","error:", response);
  100. reject(response);
  101. },
  102. onload: async function(response) {
  103. let dat;
  104. try {
  105. dat = JSON.parse(response.responseText);
  106. cont.innerHTML=`Loading...(p${-pageCheckCounter})`; //display progress
  107.  
  108. if(dat.status=="error" && dat?.errorDetails?.csrf){ //valid csrf token required
  109. token="expired"; //set it to invalid to show user error
  110. GM.setValue("token",token);
  111. updateHTML(); //display to user
  112. reject(dat); //cancel request
  113. return;
  114. }
  115.  
  116. if(settings.dynLoad){ //dynamic loading: stop requesting new pages when a known notification is discovered
  117. for (const el of dat.messages) {
  118. if(messages.some(msg=>msg.id==el.messageId)){ //identify by messageId
  119. resolve(dat);
  120. return;
  121. }
  122. }
  123. }
  124.  
  125. if (--pageCheckCounter!=0 && dat.hasMore) { //hasMore is true if another page exists. Unless user-defined max page-request is reached (pageCheckCounter==0)
  126. request(dat.cursor).then(nret => { //recursive call with next cursor/page
  127. dat.messages = dat.messages.concat(nret.messages); //callback: merge results
  128. resolve(dat);
  129. return;
  130. });
  131. } else { //if this is the last/only notification page to check
  132. resolve(dat);
  133. }
  134. } catch (e) {
  135. reject(e);
  136. }
  137. }
  138. });
  139. });
  140. }
  141.  
  142. //initial call after storage is loaded (GM.getvalue)
  143. function init(){
  144. if(location.href.includes("deviantart.com")){ //fetch token when on deviantart.com and cancel
  145. let token=document.querySelector("input[name=validate_token]")?.value??token;
  146. GM.setValue("token",token);
  147. return;
  148. }
  149.  
  150. injectHTML(); //inject sidebar into page
  151.  
  152. if(settings.checkInterval>0 || settings.checkOnPageLoad)timer(); //request update (on pageload or initial)
  153. else updateHTML(); //update only manually: just display internal storage
  154.  
  155. if(settings.checkInterval>0)setInterval(timer,settings.checkInterval*1e3); //request update in intervals
  156. }
  157.  
  158. //generates text messages for notification objects. returns HTML code to be displayed as entry
  159. //note: contains only observed objects. there might be more, especially for commissions/group admins.
  160. // will default to "Action of regarding {type}" and console.log the full object to be reported
  161. //note: Some objects not always contain the members, hence the object?.member??"" construct
  162. function convertMsgText(obj){
  163. try{
  164. let origNam=`<a class='dA_sb_user' target='_blank' href='https://www.deviantart.com/${obj.originator.username}'>${obj.originator.username}</a>`;
  165. let devTitl=(titl,url=null)=>`<${url==null?"span":"a target='_blank' href='"+url+"'"} class='dA_sb_title'>${titl}</${url==null?"span":"a"}>`;
  166. let comLink=(text,url=null)=>`<${url==null?"span":"a target='_blank' href='"+url+"'"} class='dA_sb_com'>${text}</${url==null?"span":"a"}>`;
  167. switch(obj.type){
  168. case "nc.fragments_replenish_receipt":
  169. return `You received ${devTitl(obj.messageData.fragmentsReplenishReceipt?.profit??"")} fragments.`;
  170. case "nc.fave":
  171. return `${origNam} added ${devTitl(obj.messageData.fave?.deviation?.title??"?",obj.messageData.fave?.deviation?.url)} to their favourites.`;
  172. case "nc.private_collect":
  173. return `${origNam} added ${devTitl(obj.messageData.privateCollect?.deviation?.title??"?",obj.messageData.privateCollect?.deviation?.url)} to their private collection.`;
  174. case "nc.comment_liked":
  175. if(obj.messageData.comment?.comment?.commentable?.dataKey=="profile")return `${origNam} liked your comment their profile.`;
  176. else 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)}.`;
  177. case "nc.replied":
  178. if(obj.messageData.replied?.comment?.commentable?.dataKey=="profile")return `${origNam} replied to your comment on your profile.`;
  179. else 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)}.`;
  180. case "nc.badge_given":
  181. return `${origNam} gave you a ${devTitl(obj.messageData.badgeGiven?.badge?.title??"")} badge.`;
  182. case "nc.group_join_request_receipt":
  183. return `Your group membership in ${origNam} is currently on vote.`;
  184. case "nc.comment_mentions_deviation":
  185. return `${origNam} ${comLink("mentioned",obj.messageData.commentMentionsDeviation?.mentioner?.commentUrl)} your deviation ${devTitl(obj.messageData.commentMentionsDeviation?.mentioned?.title??"?",obj.messageData.commentMentionsDeviation?.mentioned?.url)}.`;
  186. case "nc.comment":
  187. 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)}.`;
  188. case "nc.collect":
  189. return `${origNam} added your work ${devTitl(obj.messageData.collect?.deviation?.title??"?",obj.messageData.collect?.deviation?.url)} to their collection.`;
  190. case "nc.deviation_mentions_deviation":
  191. 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.`;
  192. case "nc.new_watcher":
  193. return `${origNam} is now watching you!`;
  194. case "nc.deviation_submission_offer_artist_receipt":
  195. return `${origNam} accepted your group submission ${devTitl(obj.messageData.correspondence?.bppModule?.groupDeviation?.deviation?.title??"",obj.messageData.correspondence?.bppModule?.groupDeviation?.deviation?.url)}`;
  196. case "nc.blog_submission_author_receipt":
  197. return `${origNam} posted a new blog${devTitl(obj.messageData.correspondence?.bppModule?.groupBlog?.deviation?.title??"?",obj.messageData.correspondence?.bppModule?.groupBlog?.deviation?.url)}!`;
  198. case "nc.radom_recommendation":
  199. return `Please welcome the new user ${origNam}!`;
  200. case "nc.group_created":
  201. return `Group ${origNam} created!`;
  202. case "nc.award_badge_given_on_deviation":
  203. 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)??""}.`;
  204. case "nc.award_badge_given_on_comment":
  205. return `${origNam} gave you a ${devTitl(obj.messageData.awardBadgeGivenOnComment?.badge?.title)??""} badge for your your ${comLink("comment",obj.messageData.awardBadgeGivenOnComment?.comment?.commentUrl)}!`;
  206. default:
  207. if(obj.originator.username){
  208. return `Action of ${origNam} regarding ${obj.type}`;
  209. } else{
  210. return `Action of regarding ${obj.type}`;
  211. }
  212. }
  213. }catch(ex){
  214. console.log("dA_Sidebar3 unknown:",ex,obj.type,JSON.stringify(obj));
  215. return `Action of regarding ${obj.type}`;
  216. }
  217. }
  218.  
  219. //update request timer
  220. function timer(){
  221.  
  222. // makeRequest: requests {settings.quickCheck} pages or load from storage if last request was within {checkInterval} from another page
  223. pageCheckCounter=settings.quickCheck;
  224. makeRequest().then(ret=>{
  225. if(ret==null){ //invalid csrf or already checked.
  226. updateHTML(); //update UI
  227. return;
  228. }
  229.  
  230. lastCheck=Date.now()/1e3;
  231.  
  232. if(settings.dynLoad){//dynamic load: remove old notifications with timestamp newer than the oldest message in dynamic loading
  233. 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
  234. messages=messages.filter(el=>el.ts<minTS);
  235. }else{ //without dynamic loading: discard old storage
  236. messages=[];
  237. }
  238.  
  239. ret.messages.forEach(el=>{ //add new notifications to messages-array, identified by messageId
  240. messages.push({ //messages should not be in both arrays at this point. old present messages at dyn loading will be removed by filter previously
  241. id:el.messageId,
  242. ts:el.ts, //timestamp
  243. cat:assignCat(el), //assign category by notification type
  244. msg:convertMsgText(el), //generate notification text message
  245. unread:el.isNew //read/unread from deviantart. old/new terminology in script highlight
  246. });
  247. });
  248. messages=messages.sort((a,b)=>a.ts<b.ts); //sort notifications by timestamp
  249.  
  250.  
  251. GM.setValue("messages",JSON.stringify(messages)); //update storage
  252. GM.setValue("lastCheck",lastCheck);
  253.  
  254. updateHTML();//refresh UI
  255. }).catch(ret=>{console.log("dA_Sidebar3:","An error occured:",ret)});
  256. }
  257.  
  258. //highlight or normalize sidebar, check for notifications being new
  259. function highlight(reset=false){ //reset = mark all as known
  260.  
  261. //restore default
  262. div.classList.remove("dA_Sidebar3_newBar");
  263. document.querySelectorAll(".dA_Sidebar3_newEntr").forEach(el=>el.classList.remove("dA_Sidebar3_newEntr"));
  264. document.querySelectorAll(".dA_sidebar3_entr_hot").forEach(el=>el.classList.remove("dA_sidebar3_entr_hot"));
  265.  
  266. if(reset){
  267. scrKnown=new Set(messages.map(el=>el.id));// all are known, remove unused message ids
  268. lastNotifCnt=0; //rest system notification counter
  269. GM.setValue("lastNotifCnt",lastNotifCnt); //update storage
  270. GM.setValue("messages",JSON.stringify(messages));
  271. GM.setValue("scrKnown",JSON.stringify([...scrKnown]));
  272. updateHTML();//update UI
  273. return;
  274. }
  275.  
  276. let cntNew=0; //counter of new notifications in script
  277.  
  278. messages.forEach(val=>{ //count new notification & highlight counter in sidebar
  279. if(settings.countRead && !val.unread)return; //if set, only highlight on unread messages
  280. if(scrKnown.has(val.id))return // old news in script storage
  281. ++cntNew;
  282. document.querySelector("#dA_Sidebar3 span[title='"+val.cat+"']").classList.add("dA_Sidebar3_newEntr");
  283. });
  284.  
  285. if(cntNew>0){ //highlight sidebar and show system notification
  286. div.classList.add("dA_Sidebar3_newBar")
  287.  
  288. if(settings.showNotif){
  289. GM.getValue("lastNotifCnt",0).then(ret=>{ //show notification only if not shown already for this amount of new notifications
  290. lastNotifCnt=ret;
  291. if(lastNotifCnt<cntNew){
  292. GM.notification({ title: "dA_Sidebar3",text: cntNew+" new DeviantArt notifications", url:"https://deviantart.com/notifications" });
  293. }
  294.  
  295. lastNotifCnt=cntNew; //update counter for shown system notifications
  296. GM.setValue("lastNotifCnt",lastNotifCnt);
  297. });
  298. }
  299. }
  300.  
  301. }
  302.  
  303. //opens the setting dialog and shows present settings
  304. function showSettings(){
  305. setdiv.style.display="block"; //show form
  306.  
  307. //settings loaded at pageload
  308.  
  309. //load settings
  310. let form=document.forms.dA_Sidebar3_form;
  311. form.elements.checkInterval.value=settings.checkInterval;
  312. form.elements.checkInterval.removeAttribute("readonly");
  313. if(settings.checkInterval==0){
  314. form.elements.checkInterval.value=0;
  315. form.elements.checkInterval.setAttribute("readonly","");
  316. form.elements.checkIntervalNot.checked=true;
  317. }
  318.  
  319. form.elements.quickCheck.value=settings.quickCheck;
  320. form.elements.quickCheck.removeAttribute("readonly");
  321. if(settings.quickCheck==0){
  322. form.elements.quickCheck.value=0;
  323. form.elements.quickCheck.setAttribute("readonly","");
  324. form.elements.quickCheckAll.checked=true;
  325. }
  326. form.elements.countNew.checked=settings.countRead;
  327. form.elements.checkOnPageLoad.checked=settings.checkOnPageLoad;
  328. form.elements.hideBar.checked=settings.hideBar;
  329. form.elements.barPosition[settings.barPosition].checked=true;
  330. form.elements.pulseNew.checked=settings.pulseNew;
  331. form.elements.dynLoad.checked=settings.dynLoad;
  332. form.elements.showNotif.checked=settings.showNotif;
  333. }
  334.  
  335. //close setting dialog and save chosen settings
  336. function saveSettings(){
  337. setdiv.style.display="none"; //close dialog
  338.  
  339. //save chosen settings
  340. let form=document.forms.dA_Sidebar3_form
  341. settings.checkInterval = parseInt(form.elements.checkInterval.value);
  342. if(form.elements.checkIntervalNot.checked)settings.checkInterval=0;
  343. else if(settings.checkInterval<10)settings.checkInterval=10;
  344.  
  345. settings.quickCheck = form.elements.quickCheck.value;
  346. if(form.elements.quickCheckAll.checked)settings.quickCheck=0;
  347.  
  348. settings.countRead = form.elements.countNew.checked;
  349. settings.checkOnPageLoad = form.elements.checkOnPageLoad.checked;
  350. settings.hideBar = form.elements.hideBar.checked;
  351. settings.pulseNew = form.elements.pulseNew.checked;
  352. settings.dynLoad= form.elements.dynLoad.checked;
  353. settings.showNotif = form.elements.showNotif.checked;
  354.  
  355. document.forms.dA_Sidebar3_form.elements.barPosition.forEach((el,ind)=>{if(el.checked)settings.barPosition=ind});
  356.  
  357. //store settings in storage
  358. GM.setValue("settings",JSON.stringify(settings));
  359.  
  360. updateHTML();
  361. insertStyle(); //update view
  362. }
  363.  
  364. //helper: parse as int, even NaN, and return at least minimum
  365. function cropmin(val,min){
  366. let intval=parseInt(val);
  367. if(isNaN(intval)||intval<min)return min;
  368. return intval;
  369. }
  370.  
  371. function colorTime(){ //assign CSS classes to notification entries depending on their timestamp
  372. let n=new Date();
  373. [...document.querySelectorAll("#dA_Sidebar3_popup span.dA_sidebar3_entr_tim")].forEach(el=>{
  374. let diff=(n-(new Date(el.getAttribute("ts"))))/60e3; //time difference in [minutes]
  375. if(diff<10)el.classList.add("dA_sb2_10min");
  376. else if(diff<60)el.classList.add("dA_sb2_1h");
  377. else if(diff<300)el.classList.add("dA_sb2_5h");
  378. else if(diff<1440)el.classList.add("dA_sb2_1d");
  379. else if(diff<7200)el.classList.add("dA_sb2_5d");
  380. });
  381.  
  382. }
  383.  
  384. //inject sidebar HTML and event handlers. Calls to insert setting dialog and insert style.
  385. function injectHTML(){
  386. div =document.createElement("div"); //main sidebar div
  387. div.id="dA_Sidebar3";
  388. cont =document.createElement("div"); //main content div to change via innerHTML=""
  389. cont.innerHTML=`Loading...`; //initial content while loading storage
  390. cont.addEventListener("click",()=>{ //click removes highlight
  391. highlight(true);
  392. },false)
  393. div.append(cont);
  394.  
  395. let setBut =document.createElement("button"); //setting button
  396. setBut.innerHTML=imgGear;
  397. setBut.id="dA_Sidebar3_setButton";
  398. setBut.addEventListener("click",showSettings,false);
  399. div.append(setBut);
  400.  
  401. let popupdiv=document.createElement("div"); //popup for notification texts
  402. popupdiv.id="dA_Sidebar3_popup";
  403. popupdiv.innerHTML="nothing...";
  404. div.append(popupdiv);
  405. document.body.append(div);
  406.  
  407. //event handlers
  408. div.addEventListener("mouseleave",()=>{ //hide popup when leaving sidebar
  409. let els=document.getElementById("dA_Sidebar3_popup");
  410. els.innerHTML="";
  411. },false);
  412. popupdiv.addEventListener("click",(ev)=>{ //click removes highlight
  413. if(ev.target.tagName!="A")highlight(true);
  414. },false)
  415. div.addEventListener("mouseover",(ev)=>{ //show popup with notification texts when hovering over category
  416. let els=document.getElementById("dA_Sidebar3_popup");
  417. if(ev.target.title && CatMsgs.has(ev.target.title)){ //load only pre-generated text
  418. els.innerHTML=CatMsgs.get(ev.target.title); //updated in updateHTML
  419. els.style.height="auto";
  420. els.style.bottom=div.clientHeight+"px";
  421. colorTime(); //color according to timestamp
  422. }
  423. },false);
  424.  
  425. insertSettingform(); //add setting form
  426. insertStyle(); //add CSS style
  427. }
  428.  
  429. //insert setting form HTML and event handlers
  430. function insertSettingform(){
  431.  
  432. //HTML for setting form. Initially unset and hidden. Present settings are loaded with showSettings()
  433. let settmp=`
  434. <form id='dA_Sidebar3_form'>
  435. <label for="checkInterval" title='min. 10 s'>
  436. <span>Update interval [s]</span>
  437. <input type="text" id="checkInterval" placeholder="min. 10 s" style='width:20%;'/>
  438. <label for="checkIntervalNot" style='width:24%;'>
  439. <input type="checkbox" id="checkIntervalNot" placeholder="0 = all"/>
  440. <span style='margin:0!important;'>Manual</span>
  441. </label>
  442. </label>
  443. <label for="quickCheck" title='Checks the latest 20 Notification per page.'>
  444. <span>Requested notification pages</span>
  445. <input type="text" id="quickCheck" placeholder="# of pages" style='width:20%;'/>
  446. <label for="quickCheckAll" style='width:24%;'>
  447. <input type="checkbox" id="quickCheckAll" placeholder="0 = all"/>
  448. <span style='margin:0!important;'>All</span>
  449. </label>
  450. </label>
  451. <label for="dynLoad" title='Only requests notification pages until a known message-ID is found'>
  452. <span>Dynamic request limits</span>
  453. <input type="checkbox" id="dynLoad"/>
  454. </label>
  455. <label for="countNew" title='0 = all'>
  456. <span>Show only unread notifications</span>
  457. <input type="checkbox" id="countNew"/>
  458. </label>
  459. <label for="checkOnPageLoad" title='Requests an update whenever a new page is visited'>
  460. <span>Update on pageload</span>
  461. <input type="checkbox" id="checkOnPageLoad"/>
  462. </label>
  463. <label for="hideBar" title='Hides notification bar except 2px. Hover there to show the bar again. '>
  464. <span>Hide sidebar</span>
  465. <input type="checkbox" id="hideBar"/>
  466. </label>
  467. <label for="pulseNew" title='Plays a pulse animation when new notifications appear. Click the bar to mark them as read.'>
  468. <span>Pulse animation on new notification</span>
  469. <input type="checkbox" id="pulseNew"/>
  470. </label>
  471. <label for="showNotif" title='Shows a system notification for new messages, if allowed in your browser settings.'>
  472. <span>Show system notifications</span>
  473. <input type="checkbox" id="showNotif"/>
  474. </label>
  475. <label title='Alignment of sidebar at the bottom of the window.'>
  476. <span>SideBar position</span>
  477. <label for='barPositionL'><input type="radio" id="barPositionL" name='barPosition'/><span>Left</span></label>
  478. <label for='barPositionC'><input type="radio" id="barPositionC" name='barPosition'/><span>Center</span></label>
  479. <label for='barPositionR'><input type="radio" id="barPositionR" name='barPosition'/><span>Right</span></label>
  480. </label>
  481. </form>
  482. <button type="button" id='dA_Sidebar3_saveset'>Save</button>
  483. <button type="button" id='dA_Sidebar3_cancelset'>Cancel</button>
  484. `;
  485.  
  486. setdiv=document.createElement("div"); //setting form
  487. setdiv.innerHTML=settmp;
  488. setdiv.id="dA_Sidebar3_settings";
  489. document.body.append(setdiv);
  490.  
  491. //event handlers
  492. document.getElementById("checkInterval").addEventListener("focusout",(ev)=>{ev.target.value=cropmin(ev.target.value,10);},false);//minValue checkInterval 10 [s]
  493. document.getElementById("quickCheck").addEventListener("focusout",(ev)=>{ev.target.value=cropmin(ev.target.value,0);},false);//minValue quickCheck 0 pages
  494.  
  495. //save/cancel buttons
  496. document.getElementById("dA_Sidebar3_saveset").addEventListener("click",saveSettings,false);
  497. document.getElementById("dA_Sidebar3_cancelset").addEventListener("click",()=>{setdiv.style.display="none";},false);
  498.  
  499. //checkmarks Check all pages. enable/disable text input
  500. document.getElementById("quickCheckAll").addEventListener("click",(ev)=>{
  501. if(ev.target.checked) document.getElementById("quickCheck").setAttribute("readonly","");
  502. else {document.getElementById("quickCheck").removeAttribute("readonly");document.getElementById("quickCheck").value=2;}
  503. },false);
  504. //checkmarks only manual. enable/disable text input
  505. document.getElementById("checkIntervalNot").addEventListener("click",(ev)=>{
  506. if(ev.target.checked) document.getElementById("checkInterval").setAttribute("readonly","");
  507. else{ document.getElementById("checkInterval").removeAttribute("readonly");document.getElementById("checkInterval").value=60;}
  508. },false);
  509. }
  510.  
  511. //CSS style, add as <style> in <head> or <body> if website is headless
  512. function insertStyle(){
  513. let styleText=`
  514. #dA_Sidebar3 {user-select:none;position: fixed;bottom: 0;min-width:300px;width:auto;max-width: 400px;height: auto;border: 1px solid black;
  515. ${settings.barPosition==1?"left:50%;":settings.barPosition==2?"right:0;":"left: 0;"}
  516. border-top-right-radius: 5px;font-family: Georgia;font-size: 12pt;line-height: 16pt;color: black;
  517. background: linear-gradient(#cbf9b9,#7fc458);padding: 3px;padding-right:20px;z-index:7777777;
  518. box-sizing: content-box;${settings.hideBar?"transform:translateY(100%) translateY(-5px)"+(settings.barPosition==1?" translateX(-50%);":";"):settings.barPosition==1?"transform:translateX(-50%);":""}}
  519. #dA_Sidebar3:hover{${settings.barPosition==1?"transform:translateX(-50%);":"transform:none;"}}
  520. #dA_Sidebar3.dA_Sidebar3_newBar{border:1px solid red;${settings.pulseNew?"animation: dA_Sidebar3_pulse 1s ease-out infinite":""};}
  521. #dA_Sidebar3 span.dA_Sidebar3_newEntr{color:red;}
  522. #dA_Sidebar3 *{margin:0;padding:0;}
  523. #dA_Sidebar3 img {vertical-align: middle;height: 1.4em; display: inline-block;}
  524. #dA_Sidebar3 a {cursor:pointer;color:black;text-decoration:underline;}
  525. #dA_Sidebar3>div>span {margin: 0 5px;cursor:help;white-space: nowrap;}
  526. #dA_Sidebar3 button{position: absolute;line-height: 16pt!important;background: none;border: none;cursor: pointer;}
  527. #dA_Sidebar3 button:hover{filter: invert(10%) sepia(100%) saturate(5000%) hue-rotate(359deg) brightness(150%);}
  528. #dA_Sidebar3_setButton{top: 1px;right: 1px;width:20px;height:20px;}
  529. #dA_Sidebar3_closeButton{top: -4px;right: 20px;width: 12px;height: 20px;font-size: 17px;}
  530. #dA_Sidebar3_setButton svg{vertical-align:top;}
  531. @keyframes dA_Sidebar3_pulse {
  532. 0% { box-shadow: 0 0 0 red; }
  533. 50% { box-shadow: 0 0 17px red; }
  534. 100% { box-shadow: 0 0 0 red; }
  535. }
  536. #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;}
  537. #dA_Sidebar3_settings * {vertical-align:middle;}
  538. #dA_Sidebar3_settings input[readonly]{background-color:#ccc;}
  539. #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;}
  540. #dA_Sidebar3_settings form > label > span {width: 210px; display: inline-block!important;}
  541. #dA_Sidebar3_settings label{padding: 5px 0!important;cursor:help!important;display:inline-block;}
  542. #dA_Sidebar3_settings form{display:grid;padding: 10px!important;margin-bottom:40px;}
  543. #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;}
  544. #dA_Sidebar3_settings input[type='checkbox']{cursor:pointer; width: 40%; height: 20px;margin:0; appearance: checkbox; opacity: 1;}
  545. #dA_Sidebar3_settings input[type='radio']{cursor:pointer; width: 15px; height: 15px;margin:0;vertical-align:middle; appearance: radio; opacity: 1;}
  546. #dA_Sidebar3_settings label span{margin: 0 5px!important; opacity: 1;}
  547. #dA_Sidebar3_settings form>label { border-bottom: 1px dashed gray;}
  548. #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;}
  549. #dA_Sidebar3_saveset {left:33%;background: linear-gradient(#c7e8a5, #99d01f);}
  550. #dA_Sidebar3_settings button:hover{ filter: brightness(110%);}
  551. #dA_Sidebar3_settings button:active{ filter: brightness(90%);box-shadow: 1px 1px inset;}
  552. #dA_Sidebar3_cancelset {left:66%;background:linear-gradient(#ffe3e3, #fd9c91)}
  553. #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;}
  554. #dA_Sidebar3_popup>div {margin: 0px; padding: 5px; border-bottom: 1px dashed black;position:relative;}
  555.  
  556. #dA_Sidebar3_popup .dA_sb_title{color:rgb(234, 52, 47);}
  557. #dA_Sidebar3_popup .dA_sb_user{color:blue;}
  558. #dA_Sidebar3_popup .dA_sb_com{color:darkgreen;}
  559. #dA_Sidebar3_popup .dA_sidebar3_entr_tim{position:absolute;top:-7px;right:0;font-size:7pt;color:black;}
  560. #dA_Sidebar3_popup .dA_sidebar3_entr_hot{background-color:#ff000044}
  561. #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_10min{color:#ff0000;}
  562. #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1h{color:#dd0000;}
  563. #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5h{color:#aa0000;}
  564. #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_1d{color:#990000;}
  565. #dA_Sidebar3_popup .dA_sidebar3_entr_tim.dA_sb2_5d{color:#770000;}
  566. `;
  567.  
  568. if(style==null){
  569. style=document.createElement('style');
  570. style.id='dA_Sidebar3_style';
  571. let head=document.getElementsByTagName('head')[0];
  572. if(!head)document.body.appendChild(style);
  573. else document.head.appendChild(style);
  574. }
  575.  
  576. style.innerHTML=styleText;
  577. }
  578.  
  579. //update UI and generate notification text messages for hover
  580. function updateHTML(){
  581. if(token=="expired"){ //csrf token invalid or initial call
  582. cont.innerHTML="CSRF Expired! Refresh authentification by visiting <a href='https://deviantart.com' target='_blank'>deviantart.com</a>.";
  583. }else{
  584. let cats=new Map(); //counter for new messages per category {cat:#new}
  585. CatMsgs=new Map(); // popup text messages for each category {cat:HTML-list}
  586. let sum=0; //total amount of notifications
  587.  
  588. messages.forEach((val)=>{ //count notifications for each category in {cats}, total in {sum} and generate popup text in {CatMsgs}
  589. if(settings.countRead && !val.unread)return; //ignore not new if setting is set
  590. cats.set(val.cat,(cats.get(val.cat)??0)+1); //increment Map element for category
  591. sum+=1;
  592. let tim=/(.*?)T(.*?)-.*/.exec(val.ts) //parse timestamp. It's UTC-7.
  593. let dtim=new Date(`${tim[1]} ${tim[2]} UTC-7`)
  594.  
  595. CatMsgs.set(val.cat, //concat all notificiation texts in {val.msg} for each category and add timestamp display <span>
  596. `${CatMsgs.get(val.cat)??""}
  597. <div ${!scrKnown.has(val.id)?"class='dA_sidebar3_entr_hot'":""}>${val.msg}
  598. <span class='dA_sidebar3_entr_tim ts='${val.ts}'>${dtim.toLocaleString()}
  599. </span></div>`
  600. );
  601. });
  602. //display <span> with title, text and notification count for each category in iconMap. returns HTML code
  603. let mapstr= [...iconMap].reduce((acc,[key,val])=>{
  604. return `${acc}<span title=${key}>${val??key} ${cats.get(key)??0}</span>`
  605. },"")
  606. 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
  607.  
  608. highlight();//check if there are new notifications and highlight
  609. }
  610. }
  611.  
  612. //initial entry: load storage
  613. Promise.all([
  614. GM.getValue("settings",JSON.stringify(settings)),
  615. GM.getValue("messages",JSON.stringify(messages)),
  616. GM.getValue("lastCheck",lastCheck),
  617. GM.getValue("scrKnown","[]")
  618. ]).then(res=>{ //only proceed if all is loaded
  619. let tmp=JSON.parse(res[0]); //settings
  620. Object.entries(tmp).forEach(([key,val])=>{settings[key]=val;}); //load old settings, keep unset ones
  621.  
  622. messages=JSON.parse(res[1]); //internal notification storage
  623.  
  624. lastCheck=res[2];//timestamp of last update request
  625. if(settings.checkOnPageLoad)lastCheck=0; //reset timestemp to load immediately
  626.  
  627. scrKnown=new Set(JSON.parse(res[3])); //list of old ids where no notification/highlight is sent for
  628.  
  629. init(); //entry function: insert HTML, Css and start timer.
  630. });
  631.  
  632. })();