Greasy Fork is available in English.

dA_Sidebar2

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

// ==UserScript==
// @name         dA_Sidebar2
// @namespace    phi.pf-control.de/userscripts/dA_Sidebar2
// @version      1.1
// @description  Track /watch count on all sites. See /watch counts in /watch menu and button
// @author       Dediggefedde
// @include      *
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// ==/UserScript==

//TODO
//GUI settings (CD time), 2 places: dA settings, dialog
//option visible: sidebar, watchcnt, watchbut, actcnt

(function() {
    'use strict';
    // @match        https://www.deviantart.com/notifications/watch*
    let lastReqTime = 0;
    let settings = {
        reqCoolDown: 1 * 60, //min time between requests, seconds, default 1min
        showWatchCnt: true,
        showWatchBtn: true,
        showActivityCnt: true,
        showSidebar: true
    };
    let dAObjects = {
        "all": { url: "", urlCat: "watch", button: "all", sidebar: "🔔 Activity", folder: "root", link: "#all" },
        "deviations": { url: "deviations", urlCat: "watch", button: "^deviations$", sidebar: "🎨 Deviations", folder: "all", link: "/deviations" },
        "journals": { url: "journals", urlCat: "watch", button: "(journals|posts)", sidebar: "🧾 Journals", folder: "all", link: "/journals" },
        "groupDeviations": { url: "groupDeviations", urlCat: "watch", button: "group deviations", sidebar: "🗫 Group", folder: "all", link: "/groupdeviations" },
        "polls": { url: "polls", urlCat: "watch", button: "(polls|posts)", sidebar: "📊 Polls", folder: "all", link: "/polls" },
        "forums": { url: "forums", urlCat: "watch", button: "forums", sidebar: "💬 Forums", folder: "all", link: "/forums" },
        "commissions": { url: "commissions", urlCat: "watch", button: "commissions", sidebar: "🛒 Commissions", folder: "all", link: "/commissions" },
        "status": { url: "status", urlCat: "watch", button: "(status updates|posts)", sidebar: "⏰ Status", folder: "all", link: "/status" },
        "misc": { url: "misc", urlCat: "watch", button: "misc.", sidebar: "🧸 Misc.", folder: "all", link: "/miscellaneous" },
        "feedback": { url: "", urlCat: "feedback", button: "all", sidebar: "📬 Feedback", folder: "root", link: "#feed" },
        "comments": { url: "comments", urlCat: "feedback", button: "comments", sidebar: "📝 Comments", folder: "feedback", link: "/comments" },
        "replies": { url: "replies", urlCat: "feedback", button: "replies", sidebar: "🗐 Replies", folder: "feedback", link: "/replies" },
        "mentions": { url: "mentions", urlCat: "feedback", button: "mentions", sidebar: "📣 Mentions", folder: "feedback", link: "/mentions" },
        "activity": { url: "activity", urlCat: "feedback", button: "activity", sidebar: "🚀 Activity", folder: "feedback", link: "/activity" },
        "correspondence": { url: "correspondence", urlCat: "feedback", button: "correspondence", sidebar: "📚 Correspondence", folder: "feedback", link: "/correspondence" },
    };

    let tmpCounter = {};
    let counter = {}; //website button text : counter
    Object.keys(dAObjects).forEach(function(key, index) { //reset counter
        tmpCounter[key] = 0;
        counter[key] = 0;
    });

    let watchDropDown;
    let footer;
    let settingDiag;

    //deviatnart API request, recursive for offset, type one of types-keys.
    //Promise and updates counter and lastReqTime
    function updateMessages(objKey, offset = 0) {
        let murl = "https://www.deviantart.com/_napi/da-messagecentre/api/" + dAObjects[objKey].urlCat + "?limit=24&messagetype=" + dAObjects[objKey].url + "&offset=" + offset; //
        return new Promise(function(resolve, reject) {
            lastReqTime = (new Date()).getTime() / 1e3;
            GM.setValue("lastReqTime", lastReqTime);
            if (dAObjects[objKey].url == "") {
                return resolve();
            }
            GM.xmlHttpRequest({
                method: "GET",
                url: murl,
                onerror: function(response) {
                    return reject(response);
                },
                onload: function(response) {
                    try {
                        if (response.status == 200) {
                            let l = JSON.parse(response.responseText)
                            let total = 0;

                            if (!l.settings.stacked) {
                                total = parseInt(l.counts.total);
                                tmpCounter[objKey] += total;
                                if (dAObjects[objKey].folder != "root") {
                                    tmpCounter[dAObjects[objKey].folder] += total;
                                }
                                return resolve();
                            } else {
                                l.results.forEach(el => {
                                    if (el.hasOwnProperty("stackCount")) {
                                        total += parseInt(el.stackCount);
                                    } else {
                                        ++total;
                                    }
                                });

                                tmpCounter[objKey] += total;

                                if (dAObjects[objKey].folder != "root") {
                                    tmpCounter[dAObjects[objKey].folder] += total;
                                }

                                if (l.hasMore == true) {
                                    updateMessages(objKey, offset + 24).then(() => { return resolve() });
                                    return;
                                }
                                return resolve();
                            }
                            return;
                        }
                        return reject(response);
                    } catch (ex) {
                        console.log(ex);
                        return reject(response);
                    }
                }
            });
        });
    }
    //helper: get DOM matching selector and textContent
    function getElementsByText(selector, tex) {
        let rex = new RegExp("^" + tex + "$", "i");
        return Array.prototype.slice.call(document.querySelectorAll(selector)).filter(function(el) {
            return rex.test(el.innerHTML);
            //return el.textContent.toLowerCase() ===tex;
        })
    }
    //helper: only overwrites present object members
    function updateObject(oldObj, newObj) {
        Object.keys(oldObj).forEach((key, ind) => { //backward compatibility in case settings change
            if (newObj.hasOwnProperty(key)) {
                oldObj[key] = newObj[key];
            }
        });
        return oldObj;
    }

    //cls: style. string for classname, object for copy computated font properties.
    function createCntEl(id, cnt, cls) {
        let newel = document.createElement("span"); //add span with count
        if (typeof cls == "string") {
            newel.className = cls;
        } else {
            let sty = window.getComputedStyle(cls, null);
            Object.values(sty).filter(re => { return re.includes("font") }).forEach(key => {
                newel.style[key] = sty[key];
            });
            newel.style.color = sty.getPropertyValue("color");
        }
        newel.classList.add("dASidebar2_cnt");
        newel.setAttribute("dASidebar2_span", id);
        newel.setAttribute("cnt", cnt); //dAObjects shares key with counter
        newel.innerHTML = "(" + cnt + ")";
        return newel;
    }

    function updateCntEl(cntEl, cnt) {
        if (parseInt(cntEl.getAttribute("cnt")) != cnt) {
            cntEl.innerHTML = " (" + cnt + ")";
            cntEl.setAttribute("cnt", cnt);
            cntEl.classList.add("dASidebar2_updated");
            updateSidebar();
        }
    }

    function updateSidebar() {
        if (!footer) return;
        footer.classList.add("dASidebar2_alarm");
    }
    //update dA DOM buttons to show the numbers
    //expects counter[] to be filled correctly
    function updateDOM() {
        if (settings.showWatchCnt && /https:\/\/www\.deviantart\.com.*/.test(location.href)) {
            newWatchButton();
        }
        if (settings.showWatchBtn && /https:\/\/www\.deviantart\.com\/notifications\/watch.*/.test(location.href)) {
            newWatchMenu();
        }
        if (settings.showActivityCnt && /https:\/\/www\.deviantart\.com\/notifications\/feedback.*/.test(location.href)) {
            newFeedbackMenu()
        }
        if (settings.showSidebar && !/https:\/\/www\.deviantart\.com.*/.test(location.href) && window.top == window.self) {
            newSideBar();
        }
    }

    function newWatchButton() {
        let tarbut = getElementsByText("header a", "watch");
        if (tarbut.length == 0) return;
        tarbut = tarbut[0];
        if (tarbut.getAttribute("dASidebar2_button")) {
            updateCntEl(tarbut.parentNode.querySelector("span.dASidebar2_cnt"), counter.all);
        } else {
            tarbut.setAttribute("dASidebar2_button", 1);
            tarbut.parentNode.insertBefore(createCntEl("all", counter.all, tarbut), tarbut.nextSibling);
            tarbut.parentNode.style["white-space"] = "nowrap";

            watchDropDown = document.createElement("div");
            watchDropDown.id = "dASidebar2_DDWrap";
            Object.keys(dAObjects).filter(key => { return dAObjects[key].urlCat == "watch" }).forEach(key => {
                let newel = document.createElement("div");
                newel.innerHTML = dAObjects[key].sidebar + " ";
                newel.setAttribute("dASidebar2_div", key);
                newel.appendChild(createCntEl(key, counter[key], ""));
                watchDropDown.appendChild(newel);
            });
            watchDropDown.style.display = "none";
            tarbut.parentNode.insertBefore(watchDropDown, tarbut.nextSibling);
        }
        watchDropDown.addEventListener("mouseenter", evt => {
            watchDropDown.style.display = "block";
        });
        watchDropDown.addEventListener("mouseleave", evt => {
            watchDropDown.style.display = "none";
        });
        watchDropDown.addEventListener("click", evt => { //delegation
            let key = evt.target.getAttribute("dASidebar2_div");
            if (!key || !Object.keys(dAObjects).includes(key)) return;
            window.location.href = "https://www.deviantart.com/notifications/watch" + dAObjects[key].link;
        });
        tarbut.addEventListener("mouseenter", evt => {
            watchDropDown.style.display = "block";
        });
        tarbut.parentNode.addEventListener("mouseleave", evt => {
            watchDropDown.style.display = "none";
        });
    }

    function newWatchMenu() {
        if (getElementsByText("button", "deviations").length == 0) {
            return;
        }
        Object.keys(dAObjects).reverse().filter(key => { return dAObjects[key].urlCat == "watch" }).forEach(key => {
            let tbuts = getElementsByText("button,span", dAObjects[key].button); //terrible deviantart Dom structure
            tbuts.forEach(tarbut => {
                if (tarbut.getAttribute("dASidebar2_button")) return;
                tarbut.setAttribute("dASidebar2_button", 1);
                tarbut.parentNode.insertBefore(createCntEl(key, counter[key], tarbut.className), tarbut.nextSibling);
                tarbut.parentNode.style["white-space"] = "nowrap";
            });
            document.querySelectorAll("[dASidebar2_span='" + key + "']").forEach(newel => {
                updateCntEl(newel, counter[key]);
            });
        });
    }

    function newFeedbackMenu() {
        if (getElementsByText("button", "comments").length == 0) {
            return;
        }
        //console.log(counter);
        Object.keys(dAObjects).reverse().filter(key => { return dAObjects[key].urlCat == "feedback" }).forEach(key => {
            let tbuts = getElementsByText("button,span", dAObjects[key].button); //terrible deviantart Dom structure
            tbuts.forEach(tarbut => {
                if (tarbut.getAttribute("dASidebar2_button")) return;
                tarbut.setAttribute("dASidebar2_button", 1);
                tarbut.parentNode.insertBefore(createCntEl(key, counter[key], tarbut.className), tarbut.nextSibling);
                tarbut.parentNode.style["white-space"] = "nowrap";
            });
            document.querySelectorAll("[dASidebar2_span='" + key + "']").forEach(newel => {
                updateCntEl(newel, counter[key]);
            });
        });
    }

    function newSideBar() {
        let but, subbut, subel;
        if (!footer) {
            footer = document.createElement("div");
            footer.id = "dA_sidebar2_footer";
            Object.keys(dAObjects).filter(key => { return dAObjects[key].folder == "root"; }).forEach(key => {
                but = document.createElement("div");
                but.innerHTML = dAObjects[key].sidebar;
                but.className = "dA_sidebar2_rootmenu";
                but.appendChild(createCntEl(key, counter[key], ""));
                subbut = document.createElement("div");
                subbut.className = "dA_sidebar2_submenu";
                subbut.style.display = "none";
                subbut.setAttribute("folder", key);
                Object.keys(dAObjects).filter(skey => { return dAObjects[skey].folder == key; }).forEach(skey => {
                    subel = document.createElement("a");
                    subel.href = "https://www.deviantart.com/notifications/" + dAObjects[skey].urlCat + dAObjects[skey].link;
                    subel.title = dAObjects[skey].url;
                    subel.innerHTML = dAObjects[skey].sidebar;
                    subel.appendChild(createCntEl(skey, counter[skey], ""));
                    subbut.appendChild(subel);
                });
                but.appendChild(subbut);
                footer.appendChild(but);
                but.addEventListener("mouseenter", evt => {
                    evt.target.querySelector("div.dA_sidebar2_submenu").style.display = "block";
                });
                but.addEventListener("mouseleave", evt => {
                    evt.target.querySelector("div.dA_sidebar2_submenu").style.display = "none";
                });
            });
            but = document.createElement("div");
            but.innerHTML = "🔧";
            but.title = "Settings";
            but.className = "dA_sidebar2_rootmenu";
            but.style = "cursor:pointer";
            but.addEventListener("click", ev => {
                if (settingDiag) {
                    settingDiag.style.display = "";
                }
            });
            footer.appendChild(but);
            document.body.appendChild(footer);

            settingDiag = document.createElement("div");
            settingDiag.innerHTML = `
			<div>Setting</div>
			<label for="dASidebar2_ReqCD">Request Cooldown (minutes)</label>
			<input type="text" id="dASidebar2_ReqCD" value="${settings.reqCoolDown/60}" />
			<label for="dASidebar2_WatchBtn">Show counter next to Watch-button</label>
			<input type="checkbox" id="dASidebar2_WatchBtn" ${settings.showWatchBtn?"checked":""} />
			<label for="dASidebar2_WatchCnt">Show counters in Watch-menu</label>
			<input type="checkbox" id="dASidebar2_WatchCnt" ${settings.showWatchCnt?"checked":""} />
			<label for="dASidebar2_ActCnt">Show counters in Feedback-menu</label>
			<input type="checkbox" id="dASidebar2_ActCnt" ${settings.showActivityCnt?"checked":""} />
			<label for="dASidebar2_ShowSide">Show Sidebar</label>
			<input type="checkbox" id="dASidebar2_ShowSide" ${settings.showSidebar?"checked":""} />
			<button type="button" id="dASidebar2_SaveSet">Save</button>
			`;
            settingDiag.className = "dASidebar2_settings";
            settingDiag.style.display = "none";
            document.body.appendChild(settingDiag);
            document.getElementById("dASidebar2_SaveSet").addEventListener("click", evt => {
                evt.preventDefault();
                evt.stopPropagation();
                settingDiag.style.display = "none";

                settings.reqCoolDown = parseInt(document.getElementById("dASidebar2_ReqCD").value) * 60;
                settings.showWatchBtn = document.getElementById("dASidebar2_WatchBtn").checked;
                settings.showWatchCnt = document.getElementById("dASidebar2_WatchCnt").checked;
                settings.showActivityCnt = document.getElementById("dASidebar2_ActCnt").checked;
                settings.showSidebar = document.getElementById("dASidebar2_ShowSide").checked;
                GM.setValue("settings", JSON.stringify(settings));
            });

        } else {
            Object.keys(counter).forEach(key => {
                footer.querySelectorAll("[dASidebar2_span='" + key + "']").forEach(newel => {
                    updateCntEl(newel, counter[key]);
                });
            });
        }

    }


    //requests API for /watch numbers
    function updateAllNumbers() {
        let reqChain = []; //promise array
        tmpCounter = {};
        Object.keys(dAObjects).forEach(function(key, index) {
            reqChain.push(updateMessages(key)); //new promises for request
            tmpCounter[key] = 0; //reset counter
        });

        Promise.all(reqChain).then(ev => { //execute requests
            GM.setValue("counter", JSON.stringify(tmpCounter)); //save result
            counter = tmpCounter; //overwrite local counter
            updateDOM(); //update display
        }).catch(ex => { console.log(ex); });

    }

    function insertSettingMenu() {
        let setMenuCont = document.getElementById("settings_public").parentNode; //li element
        let newMen = document.createElement("li");
        newMen.innerHTML = "<a>dA_Sidebar2</a>";
        newMen.style = "cursor:pointer";
        newMen.addEventListener("click", evt => {
            evt.stopPropagation();
            evt.preventDefault();
            document.querySelectorAll("ul.menu_holder a.active").forEach(el => { el.classList.remove("active"); });
            evt.target.classList.add("active");
            document.querySelector("div.settings_form").innerHTML = `
					<div class="fooview ch dA_Sidebar2_dAsettings">
						<div class="fooview-inner">
							<h3>dA_Sidebar2 Settings</h3>
							<div class="altaltview altaltview-wider">
								<div class="row">
									<div class="browse-sitback row">
										<input type="text" id="dASidebar2_ReqCD" value="${settings.reqCoolDown/60}" />
										<label for="dASidebar2_ReqCD" class="l">Cooldown</label><br/>
										 <small>Request Cooldown (in minutes)</small>
									</div>
									<div class="browse-sitback row">
										<input type="checkbox" id="dASidebar2_WatchBtn" ${settings.showWatchBtn?"checked":""} />
										<label for="dASidebar2_WatchBtn" class="l">watch button</label><br/>
										<small>Show counter next to Watch-button</small>
									</div>
									<div class="browse-sitback row">
										<input type="checkbox" id="dASidebar2_WatchCnt" ${settings.showWatchCnt?"checked":""} />
										<label for="dASidebar2_WatchCnt" class="l">Watch counters</label><br/>
										<small>Show counters in Watch-menu</small>
									</div>
									<div class="browse-sitback row">
										<input type="checkbox" id="dASidebar2_ActCnt" ${settings.showActivityCnt?"checked":""} />
										<label for="dASidebar2_ActCnt" class="l">Feedback Counters</label><br/>
										<small>Show counters in Feedback-menu</small>
									</div>
									<div class="browse-sitback row">
										<input type="checkbox" id="dASidebar2_ShowSide" ${settings.showSidebar?"checked":""} />
										<label for="dASidebar2_ShowSide" class="l">Sidebar</label><br/>
										<small>Show sidebar on other websites</small>
									</div>
									<div class="browse-sitback row">
										<div class=" buttons ch hh " id="submit">
											<div style="text-align:right" class="rr">
												<a class="smbutton smbutton-green" href="javascript:void(0)">
													<span id="dASidebar2_savesettings">Save</span>
												</a>
											</div>
										</div>
									</div>
								</div>
							</div>
						</div>
					</div>`.replace(/\s\s+/, "");
            document.getElementById("dASidebar2_savesettings").addEventListener("click", evt => {
                evt.preventDefault();
                evt.stopPropagation();

                settings.reqCoolDown = parseInt(document.getElementById("dASidebar2_ReqCD").value) * 60;
                settings.showWatchBtn = document.getElementById("dASidebar2_WatchBtn").checked;
                settings.showWatchCnt = document.getElementById("dASidebar2_WatchCnt").checked;
                settings.showActivityCnt = document.getElementById("dASidebar2_ActCnt").checked;
                settings.showSidebar = document.getElementById("dASidebar2_ShowSide").checked;
                GM.setValue("settings", JSON.stringify(settings));

                alert("dA_Sidebar2 settings saved!");
            })
        });
        setMenuCont.parentNode.insertBefore(newMen, setMenuCont.nextSibling);
    }

    //changing dA DOM using js requires periodic check of DOM
    //watchdog checks once a second
    function watchdog() {
        GM.getValue("lastReqTime", 0).then(t => {
            lastReqTime = t;
            if (t == 0 || (new Date()).getTime() / 1e3 - lastReqTime > settings.reqCoolDown) { //Check request cooldown
                updateAllNumbers();
            } else { //update dom without request (js navigation, update in different tab)
                GM.getValue("counter", "{}").then(cnt => { //use counter from last request
                    let newCnt = JSON.parse(cnt);
                    counter = updateObject(counter, newCnt);
                    updateDOM();
                });
            }
        });
    }

    //script start
    GM.addStyle(`
    #dA_sidebar2_footer{
		position:fixed;
		bottom:0;
		margin:0;
		left:2px;
        height:25px;
		background-color:#ffd;
		border-radius:5px;
		border: 1px solid black;
        z-index:7777777;
		transform:translateY(90%);
		transition: 0.5s ease-in-out;
        color:#002;
		font-family: Verdana, serif;
		font-size:10pt;
		user-select:none;
    }
    #dA_sidebar2_footer *{
        font-style :normal;
        font-variant: normal;
        font-weight:normal;
        line-height:1em;
        letter-spacing:normal;
    }
    #dA_sidebar2_footer:hover{
		transform:translateY(0%);
		transition: 0.2s ease-in-out;
	}
    #dA_sidebar2_footer div.dA_sidebar2_rootmenu {
		padding: 4px;
		margin: 0;
		display:inline-block;
		position:relative;
	}
    #dA_sidebar2_footer div.dA_sidebar2_submenu {
		position: absolute;
		bottom: 22px;
		left: 0;
		background-color: wheat;
		border: 1px solid black;
		box-shadow: 1px 1px 2px black;
		border-radius: 10px;
		white-space: nowrap;
	}
    #dA_sidebar2_footer div.dA_sidebar2_submenu a{
		text-decoration:none;
		color:black;
		display:block;
		padding: 5px;
	}
    #dA_sidebar2_footer div.dA_sidebar2_submenu a:hover{
		color:grey;
	}
    .dASidebar2_updated{
		color:red;
	}
    .dASidebar2_cnt{
		margin-left:5px;
	}
	#dASidebar2_DDWrap{
		top:52px;
		background-color:white;
		position:absolute;
		box-shadow: 1px 1px 5px rgba(0,0,0,0.6);
		border: 1px solid black;
		border-radius: 10px;
		white-space: nowrap;
        color:rgb(6, 7, 13);
	}
    #dASidebar2_DDWrap>div{
		padding:7px;
		display:block;
		cursor:pointer;
    }
    #dASidebar2_DDWrap>div:hover{
		color:#050;
        text-decoration:underline;
    }
	#dASidebar2_DDWrap span{
		color:black
	}
    div.dASidebar2_alarm{
   		animation: blink 1s ease-in-out alternate infinite;
    }
    @keyframes blink {
    	0% {
			box-shadow: 0 0 15px 5px rgba(255,0,0,0);
  		}
   		100% {
    		box-shadow: 0 0 15px 5px rgba(255,0,0,1);
  		}
    }
	div.dASidebar2_settings {
		position: fixed;
		z-index: 777777;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background-color: #fffddd;
		border: 1px solid black;
		border-radius: 5px;
		padding: 5px;
		display: grid;
		grid-template-columns: auto auto;
		grid-gap: 10px;
		justify-items: stretch;
		align-items: center;
        box-shadow: 1px 1px 5px rgba(0,0,0,0.5);
	}
    div.dASidebar2_settings *{
		color:#002;
		font-family: Verdana, serif;
		font-size:10pt;
        font-style :normal;
        font-variant: normal;
        font-weight:normal;
        line-height:1em;
        letter-spacing:normal;
    }
	div.dASidebar2_settings input{
		justify-self:center;
    }
	div.dASidebar2_settings input,div.dASidebar2_settings label, div.dASidebar2_settings button {
		cursor:pointer;
	}
	div.dASidebar2_settings * {
		margin: 0;
		padding: 0;
	}
	#dASidebar2_ReqCD{
		width:40px;
        height:1.5em;
		text-align:center;
		cursor:text;
	}
	div.dASidebar2_settings div{
		grid-column:span 2;
		font-size:14pt;
		text-align: center;
	}
	div.dASidebar2_settings button{
		grid-column:span 2;
        background-color: #adffad;
        border-radius: 10px;
        width: 80%;
        margin: auto;
        padding: 5px;
	}
	div.dASidebar2_settings button:hover{
        background-color: #2edb75;
	}
	div.dA_Sidebar2_dAsettings intput, div.dA_Sidebar2_dAsettings labels{
		cursor:pointer;
	}
	`)
    GM.getValue("settings", "{}").then(set => { //load settings, then do stuff
        let newSet = JSON.parse(set);
        settings = updateObject(settings, newSet); //backward compatibility in case settings change

        if (!settings.showSidebar && !/deviantart.com/.test(location.href)) {
            return;
        }
        if (/deviantart.com\/settings/.test(location.href)) {
            insertSettingMenu();
        }
        setInterval(watchdog, 1000); //start watchdog

        let forceNavi = location.href.match(/https:\/\/www\.deviantart\.com\/notifications\/watch.*#(.*)/i);
        if (forceNavi != null) {
            if (forceNavi[1] == "all") getElementsByText("button", "all")[0].click()
        }
    });

})();