Greasy Fork

Greasy Fork is available in English.

dA_FilterNotifications

filter notifications by type

当前为 2022-05-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         dA_FilterNotifications
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  filter notifications by type
// @author       Dediggefedde
// @match        https://www.deviantart.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deviantart.com
// @grant        GM.addStyle
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

//@ts-check
/**
	Adds a bar on deviantart /notification page and sidebar
	The bar provides buttons to filter and search within displayed elements

	TODO: trigger fill if filter reduces elements
*/

(function() {
    "use strict";

    GM.addStyle(`
	#dA_FN_bar{display:flex;position: sticky;top: 0;z-index: 1;background-color: var(--g-bg-primary);}
	#dA_FN_bar>div{cursor:pointer;user-select:none}
	#dA_FN_bar>div:hover{filter: brightness(120%) saturate(150%) invert(20%);}
	#dA_FN_filterText{height: 1.2em;}
	.da_FN_selected{border:1px solid red;}
	.dA_FN_hidden{display:none;}
	.dA_FN_markHidden .dA_FN_hidden{display:block;}
	.dA_FN_markHidden .dA_FN_hidden button{ background-image:repeating-linear-gradient(45deg, #ffffff3b 0%, white 2%, #fdfdfd3b 2%,#c1c1c100 4%, white 4%);}
	.dA_FN_filtered{display:none;}
	#dA_FN_HideSel[active='2'] ellipse { fill: lightgray;}/*2: hiding=grey*/
	#dA_FN_HideSel[active='2'] ellipse[role='iris'] { fill: lightgray;stroke:lightgray}/*2: hiding=grey*/
	#dA_FN_HideSel[active='0'] ellipse[role='iris'] { fill: lightgray;stroke:red} /*0: hide selected=red*/
				.da_fN_notSelect{user-select:none!important}
`);

    //const itemClasses = ["_375AY", "_2TKoM", "_3PCaz", "_2VY4V"]; //possible classes for notification list item div
    const itemClassXPath = "//section//div[@data-bucket]/parent::div";
    let itemClass = "_375AY";
    const filterInterval = 200; //ms
    let filterTimer;
    /**@type {HTMLElement} */
    let cont = null; //container of items

    let selRec = { left: 0, top: 0, width: 0, height: 0 }; //div element of selection rectangle
    let selAnc = { x: 0, y: 0 }; //initial point where you started selecting
    let dragSel = false; //selection rectangle visible

    //button images
    const commBtnSvg = '<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill-rule="evenodd" d="M20 3H6.414a1 1 0 00-.707.293L3.293 5.707A1 1 0 003 6.414V16a1 1 0 001 1h3v3a1 1 0 001 1h1.5a1 1 0 00.8-.4L13 17h4.586a1 1 0 00.707-.293l2.414-2.414a1 1 0 00.293-.707V4a1 1 0 00-1-1z"></path></svg>';
    const llamaBtnSvg = '<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M17 5v7h1v3h-1v6h-3v-3h-1v3H9v-1H8v-3H7v-2H6v-2h1v-1h1v-1h4V5h-1V4h1V3h2v1h1V3h2v1h1v1h-1z"></path></g></svg>';
    const devBtnSvg = '<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 3a1 1 0 011 1v15.674a1 1 0 01-1.275.962L12 19l-5.725 1.636A1 1 0 015 19.674V4a1 1 0 011-1h12zm-3 3h-1.763l-.175.183-.832 1.635-.262.182H9v2.497h1.632l.145.181L9 14.182V16h1.763l.176-.183.831-1.635.262-.182H15v-2.497h-1.632l-.145-.183L15 7.818V6z" fill-rule="evenodd"></path></svg>';
    const hideBtn = '<svg width="24" height="24"  xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 250" stroke-width="20"> <defs> <radialGradient id="c1" cx="0.5" cy="0.5" r="0.5"> <stop offset="0" stop-color="#ffffff" /> <stop offset=".5" stop-color="hsl(40, 60%, 60%)" /> <stop offset="1" stop-color="#3dff3d" /> </radialGradient> </defs> <ellipse role="sclera" cx="250" cy="125" rx="200" ry="100" fill="white"/> <ellipse role="iris" cx="250" cy="125" rx="95" ry="95" stroke="black" fill="url(#c1)"/> <ellipse role="pupil" cx="250" cy="125" rx="50" ry="50" stroke="none" fill="black"/> <ellipse role="light" cx="200" cy="80" rx="50" ry="50" stroke="none" fill="#fffffaee"/> <ellipse role="outline" cx="250" cy="125" rx="200" ry="100" stroke="black" fill="none"/> </svg>'

    const fm = { show: 0, hide: 1, only: 2 }; //filter mode
    //"011-1h16zm-1 2H5v14h14V5zm-4" part of SVG in Journal icon
    //"01.68-.182l.862.505c.2" part of SVG in Shout icon
    const ftext = { //text used as regexp for each filter
        yourComment: ".*Your comment.*",
        yourLlama: ".*(Your (Llama badges|transactions|mentions|watchers))|(011-1h16zm-1 2H5v14h14V5zm-4)|(01.68-.182l.862.505c.2).*",
        yourDevs: "<img",
        text: ".*##ftext##.*"
    };
    let filter = { //applied filter settings. text regexp /i.
        yourComment: fm.show,
        yourLlama: fm.show,
        yourDevs: fm.show,
        text: "",
        version: 0.1
    };
    let showHidden = false; //disable custom hidden buckets
    let hideBuckets = new Set(); //list of buckets to hide from view
    let shiftPressed = false; //shift pressed to prevent selection

    function improveLayout() {
        let sty = document.getElementById("dA_FN_improveCSS");
        if (sty == null) {
            let sty = document.createElement("style");
            sty.id = 'dA_FN_improveCSS';
            sty.innerHTML = `
				div[data-nc] * {visibility: visible;} /* buttons do not vanish*/
				div[data-nc] button[data-hook="user_watch_button"] {display: none;} /*removes watch/unwatch buttons*/
				[aria-label="Remove"] svg:hover {fill: red !important;} /*unify remove buttons hover color*/
				div[data-nc*="favecollect"] > section > div > div:nth-child(2) {display: none;} /*remove activity gallery for new favourites*/
				div[data-nc*="new_watcher"] > section > div > div:nth-child(2) {display: none;} /*remove activity gallery for watchers*/
				div[data-nc*="badge_given"] > section > div > div:nth-child(2) {display: none;} /*remove activity gallery for Llamas*/
				[role="presentation"] {display: none;} /*remove background image from deviation fullview*/
				a[data-hook="deviation_link"] img {object-fit: contain;} /*make thumbnails contain full image*/
									input[type="checkbox"] + div svg { fill: #0000;}
									input[type="checkbox"]:checked + div svg { fill: #000F;}
				`
            document.head.appendChild(sty);
        }

        let it = document.evaluate("//h2[contains(., 'All Notifications')]/parent::div/parent::div/preceding-sibling::*").iterateNext(); //banner behind "all notification"
        if (it != null) {
            it.style.filter = "opacity(0.3) blur(1px)"; //option 1: less dominant title pic
            it.parentNode.parentNode.style.display = "none"; // option 2: no title pic
            it.parentNode.parentNode.nextSibling.style.marginTop = "20px";
        }

        //it=document.querySelectorAll("div[data-nc] div[role]"); //option 1: vanishing buttons now have .dA_fN_notiSideBut
        //[...it].forEach(el=>{
        //    el.classList.add("dA_fN_notiSideBut");
        //    el.nextSibling.classList.add("dA_fN_notiSideBut");
        //    el.nextSibling.nextSibling.classList.add("dA_fN_notiSideBut");
        //});

        it = document.querySelector("div[data-nc]"); //no wasted space on large screens (old limit 1024px)
        if (it != null)
            it.parentNode.style.maxWidth = "unset";

        cont.querySelector("section").style.display = "none";
        // document.evaluate(`//div[@class='${itemClass}']//parent::div//section`).iterateNext().style.display="none"; // remove tutorial ads
    }

    /**
     * btn object, mode fm-mode, texts array of text [3]
     * @param {string} btnId DOM id of button to change title and color
     * @param {number} mode new mode of filter related to button
     * @param {Array<string>} texts title text array [show,hide,only]
     */
    //
    function updateButton(btnId, mode, texts) {
        /** @type {HTMLElement} */
        let btn = document.querySelector(btnId);
        if (btn == null) return;
        switch (mode) {
            case fm.hide:
                btn.style.fill = "grey";
                break;
            case fm.only:
                btn.style.fill = "red";
                break;
            case fm.show:
                btn.style.fill = "";
                break;
        }
        btn.title = texts[mode];
    }

    /** adapts GUI to settings */
    function updateGUI() {
        updateButton("#dA_FN_hideYourCom", filter.yourComment, [
            "Your comments are shown",
            "Your comments are hidden",
            "Only your comments are shown"
        ]);
        updateButton("#dA_FN_hideYourLlama", filter.yourLlama, [
            "Your Correspondence is shown",
            "Your Correspondence are hidden",
            "Only your Correspondence are shown"
        ]);
        updateButton("#dA_FN_hideYourDevs", filter.yourDevs, [
            "Your Deviations are shown",
            "Your Deviations are hidden",
            "Only your Deviations are shown"
        ]);

        /**@type {HTMLDivElement} */
        let hidespan = document.querySelector("#dA_FN_HideSel");
        if (document.getElementsByClassName("da_FN_selected").length == 0) {
            if (showHidden) {
                hidespan.title = "Hide Hidden";
                hidespan.setAttribute("active", "1");
                cont.classList.add("dA_FN_markHidden");
            } else {
                hidespan.title = "Show Hidden";
                hidespan.setAttribute("active", "2");
                cont.classList.remove("dA_FN_markHidden");
            }
        } else {
            if (showHidden) {
                hidespan.title = "Unhide";
            } else {
                hidespan.title = "Hide";
            }
            hidespan.setAttribute("active", "0");
        }
    }

    /**
     * Event Handler Filter Button Click
     * Iterates show-hide-only-show
     * updates GUI, applies filter
     * @param {*} ev Mouse Click Event
     * @param {*} filt related filter in fm
     */
    function btnIterateStateClick(ev, filt) {
        filter[filt] = (filter[filt] + 1) % 3; //iterate: show-hide-only-show

        if (filter[filt] == fm.only) {
            filter.yourComment = fm.show;
            filter.yourDevs = fm.show;
            filter.yourLlama = fm.show;

            filter[filt] = fm.only;
        }

        updateGUI();
        filterDOMList(); //apply filter
        saveSettings();
    }

    /** @type {Array<HTMLElement>} */
    let listEls = [];

    /** refreshes internal list of DOM elements */
    function grabList() {
        listEls = Array.from(document.querySelectorAll(`.${itemClass}`));
    }

    /**
     * Apply filter to DOM elements in listEls
     * Text filter uses regexp.
     * TODO: does not trigger "load more" when list gets smaller than sidebar
     *  */
    function filterDOMList() {
        /** @type {Array<HTMLElement>} */
        let elsA = []; //to be filled with elements to hide
        if (listEls.length == 0) return;

        listEls.forEach(el => {
            el.classList.remove("dA_FN_filtered");
            el.classList.remove("dA_FN_hidden");
        });

        //iterate through filters, Or-Connect list of elements to hide, fill elsA (iterateNext fails if DOM is changed here)
        Object.entries(filter).forEach(fel => {
            if (fel[0] == "text" && fel[1] != "") {
                let reg = new RegExp(fel[1], "i");
                elsA = elsA.concat(
                    listEls.filter(lisEl => {
                        return !reg.test(lisEl.innerHTML);
                    })
                );

            } else {
                let reg = new RegExp(ftext[fel[0]]);
                elsA = elsA.concat(
                    listEls.filter(lisEl => {
                        let regtst = reg.test(lisEl.innerHTML);
                        return (fel[1] == fm.only && !regtst) ||
                            (fel[1] == fm.hide && regtst);
                    })
                );
            }

        });

        //hide elements
        elsA.forEach((el) => {
            el.classList.add("dA_FN_filtered");
        });

        //hide custom hidden elements
        hideBuckets.forEach(el => {
            /**@type{HTMLElement} */
            let bck = document.querySelector(`[data-bucket='${el}']`);
            if (bck == null) return;
            bck = bck.closest(`.${itemClass}`);
            if (bck != null)
                bck.classList.add("dA_FN_hidden");
        });

        //load more if end of list is visible
        var lastBnds = listEls[listEls.length - 1].getBoundingClientRect();
        if (lastBnds.bottom < cont.clientHeight) {
            console.log("load more please!")
                //all shown, load of more required.
                //no idea how to trigger
        }
    }

    function isCollide(a, b, ov) {
        return !(
            ((a.top + a.height + ov) < (b.top)) ||
            (a.top > (b.top + b.height + ov)) ||
            ((a.left + a.width + ov) < b.left) ||
            (a.left > (b.left + b.width + ov))
        );
    }

    /** attachs handlers for selection rectangle and selecting itemClass  */
    function applyDragSelectHandler() {
        //the rectangle
        selRec = document.createElement("div");
        selRec.id = "dA_fM_select";
        selRec.setAttribute("style", "display:none;position:absolute;z-index:1;top:0px;bottom:0px;width:0px;height:0px;background:#a008;");
        document.body.append(selRec);

        let cntCl = `.${cont.className.replace(/\s+/gi,".")}`;

        //starting rectangle
        document.body.addEventListener("mousedown", (ev) => {
            //if (ev.target.closest(cntCl) == null) return;
            if (shiftPressed) return; //no selection when shift is pressed (text-select)
            if (ev.target.closest("#dA_FN_bar") != null) return;
            dragSel = true;
            selAnc.x = ev.clientX;
            selAnc.y = ev.clientY;
            selRec.style.left = selAnc.x + "px";
            selRec.style.top = selAnc.y + "px";
            updateSelection(ev);
            document.getElementById("root").classList.add("da_fN_notSelect");
        }, false);

        //updating rectangle
        document.body.addEventListener("mousemove", (ev) => {
            ev.stopPropagation();
            if (!dragSel) return;
            selRec.style.display = "block";

            if (ev.clientX < selAnc.x)
                selRec.style.left = ev.clientX + "px";
            selRec.style.width = Math.abs(ev.clientX - selAnc.x) + "px";

            if (ev.clientY < selAnc.y)
                selRec.style.top = ev.clientY + "px";
            selRec.style.height = Math.abs(ev.clientY - selAnc.y) + "px";
        }, false);

        //stopping rectangle
        document.body.addEventListener("mouseup", (ev) => {
            dragSel = false;
            selRec.style.display = "none";
            document.getElementById("root").classList.remove("da_fN_notSelect");
        }, false);

        //updating selection
        document.body.addEventListener("mouseenter", updateSelection, true);
        document.body.addEventListener("mouseleave", updateSelection, true);
        document.body.addEventListener("keydown", (ev) => { shiftPressed = ev.shiftKey }, false);
        document.body.addEventListener("keyup", (ev) => { shiftPressed = ev.shiftKey }, false);

    }


    /**
     * checks collision with selection rectangle and updates selection list of .itemClass
     * @param {Event} ev
     */
    function updateSelection(ev) {
        ev.stopPropagation();
        if (!dragSel) return;
        let listels = Array.from(document.querySelectorAll(`.${itemClass}`));
        let bndRec = selRec.getBoundingClientRect();
        // if (bndRec.width == 0 && bndRec.height == 0) return;
        let elRec;
        listels.forEach(el => {
            elRec = el.getBoundingClientRect();
            if (elRec.height == 0) return;
            if (isCollide(bndRec, elRec, 0)) {
                el.classList.add("da_FN_selected");
            } else {
                el.classList.remove("da_FN_selected");
            }
        });
        listels = Array.from(document.querySelectorAll(`[data-nc]`));
        listels.forEach(el => {
            elRec = el.getBoundingClientRect();
            if (elRec.height == 0) return;
            if (isCollide(bndRec, elRec, 0)) {
                if (!el.classList.contains("da_FN_selected"))
                    el.querySelector("input[type=checkbox]").parentNode.click();
                el.classList.add("da_FN_selected");
            } else {
                if (el.classList.contains("da_FN_selected"))
                    el.querySelector("input[type=checkbox]").parentNode.click();
                el.classList.remove("da_FN_selected");
            }
        });

        updateGUI();
    }

    function saveSettings() {
        GM.setValue("filter", JSON.stringify(filter));
        GM.setValue("hidden", JSON.stringify(Array.from(hideBuckets)));
    }

    function loadSettings() {
        GM.getValue("filter", null).then(ret => {
            let pr = JSON.parse(ret);
            if (pr != null && pr.version == "0.1")
                filter = pr;
            return GM.getValue("hidden", null);
        }).then(ret => {
            let pr = JSON.parse(ret);
            if (pr != null) hideBuckets = new Set(pr);
        });
    }

    /** called periodically to insert GUI if not present. Site uses javascript navigation. */
    function starter() {
        if (document.querySelector("#dA_FN_bar") != null) return; //already present

        let xEl = document.evaluate(itemClassXPath).iterateNext();
        if (xEl == null) return; //no place to add filter
        itemClass = xEl.className;

        cont = document.querySelector(`.${itemClass}`).parentElement;

        //add elements.
        let bar = document.createElement("div"); //container bar
        bar.id = "dA_FN_bar";

        let btnYourCom = document.createElement("div"); //button comments
        btnYourCom.id = "dA_FN_hideYourCom";
        btnYourCom.innerHTML = commBtnSvg;
        btnYourCom.addEventListener("click", (ev) => { btnIterateStateClick(ev, "yourComment"); }, false);
        bar.appendChild(btnYourCom);

        let btnYourLlama = document.createElement("div"); //button correspondence
        btnYourLlama.id = "dA_FN_hideYourLlama";
        btnYourLlama.innerHTML = llamaBtnSvg;
        btnYourLlama.addEventListener("click", (ev) => { btnIterateStateClick(ev, "yourLlama"); }, false);
        bar.appendChild(btnYourLlama);

        let btnYourDevs = document.createElement("div"); //button Deviations
        btnYourDevs.id = "dA_FN_hideYourDevs";
        btnYourDevs.innerHTML = devBtnSvg;
        btnYourDevs.addEventListener("click", (ev) => { btnIterateStateClick(ev, "yourDevs"); }, false);
        bar.appendChild(btnYourDevs);

        let spanHide = document.createElement("div");
        spanHide.id = "dA_FN_HideSel";
        spanHide.innerHTML = hideBtn;
        spanHide.addEventListener("click", (ev) => {
            let selected = Array.from(document.getElementsByClassName("da_FN_selected"));
            if (selected.length > 0) { //hide mode
                selected.forEach(el => {
                    let bck = el.querySelector("[data-bucket]");
                    if (bck == null) return;
                    let bckID = bck.getAttribute("data-bucket");
                    if (showHidden) {
                        hideBuckets.delete(bckID);
                    } else {
                        hideBuckets.add(bckID);
                    }
                    el.classList.remove("da_FN_selected");
                });
            } else { //show hidden mode
                showHidden = !showHidden;
            }

            clearTimeout(filterTimer);
            filterTimer = setTimeout(function() {
                filterDOMList();
            }, filterInterval);

            saveSettings();
            updateGUI();

        }, false);
        bar.appendChild(spanHide);

        let editFilterTex = document.createElement("input"); //search Input
        editFilterTex.id = "dA_FN_filterText";
        editFilterTex.type = "text";
        editFilterTex.addEventListener("input", (ev) => { //throttle filterTimer to filterInterval in ms, apply filter
            filter.text = ev.target.value;
            clearTimeout(filterTimer);
            filterTimer = setTimeout(function() {
                filterDOMList();
            }, filterInterval);
            saveSettings();
        }, false);
        bar.appendChild(editFilterTex);


        xEl.parentNode.insertBefore(bar, xEl); //insert container
        bar.nextSibling.style.top = "85px";

        grabList(); //prepare DOM list
        updateGUI(); //display filter setting
        filterDOMList();
        improveLayout();

        //scroll refreshes grablist and needs filter reapplied
        cont.addEventListener("scroll", function() { //throttle filterTimer to filterInterval in ms, apply filter
            if (document.getElementsByClassName(itemClass).length != listEls.length)
                grabList();
            clearTimeout(filterTimer);
            filterTimer = setTimeout(function() {
                filterDOMList();
            }, filterInterval);
        }, false);
        applyDragSelectHandler();
    }

    //start script,
    //insert new elements periodically if not present.
    loadSettings();
    setInterval(starter, 1000);
})();