// ==UserScript==
// @name dA_FilterNotifications
// @namespace http://tampermonkey.net/
// @version 0.9
// @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
// @downloadURL https://update.greasyfork.icu/scripts/445528/dA_FilterNotifications.user.js
// @updateURL https://update.greasyfork.icu/scripts/445528/dA_FilterNotifications.meta.js
// ==/UserScript==
//@ts-nocheck
/**
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}
#dA_FN_settings{width:30vw;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background-color:#edffea;display:grid;grid-template-columns: 80% auto;grid-template-rows: 3em repeat(5,2em) 7em 2em; grid-gap: 10px;padding: 5px;border-radius: 10px;border: 1px ridge;box-shadow: 2px 2px 10px #777;align-items: center;user-select:none;}
#dA_FN_customCSS {grid-column: 1 /span 2;resize:none;height:100%;}
#dA_FN_settings .dA_FN_setBtn{grid-column: 1/span 2; text-align: center;}
#dA_FN_OKBtn {width: 100px;cursor: pointer;}
#dA_FN_settings input[type=checkbox]{cursor: pointer;}
#dA_FN_settings label{cursor: pointer;width:100%;}
#dA_FN_settings h3{grid-column:1 /span 2;font-weight: bold;text-align: center;font-size: larger;}
#dA_FN_cancelBtn{cursor: pointer;position:absolute;top:2px;right:2px;background-color:transparent;border:none;font-size:larger;}
#dA_FN_cancelBtn:hover{color: #ccc;}
#dA_FN_settingBtn{position: absolute;top: 2px;right: 2px;width: 20px;height: 20px;cursor: pointer;}
`);
//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 = '';
const LlamaBtnSvg = '';
const DevBtnSvg = '';
const HideBtn = ''
const ImgGear = '';
const SettingFormTmpl = `
dA_FilterNotification Settings
`; //#dA_FN_settings
let settingForm;
let settings = { filterSide: true, dragSelect: true, updateCss: true, useCustomCss: false, customCss: "",removeSiteNotes:true };
const CSSInsertTMPL = `
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;}
`
const Fm = { show: 0, hide: 1, only: 2 }; //filter mode
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: "
{
let it = document.evaluate("//h2[contains(., 'All Notifications')]/parent::div/parent::div/preceding-sibling::*", document).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";
let readAllBut=document.evaluate("//button[contains(.,'Mark All as Read')]",document).iterateNext();
if(readAllBut!=null){
it.parentNode.parentNode.before(readAllBut);
readAllBut.style="margin:0;position:absolute;top:0;left:50%;transform:translateX(-50%);";
}
}
it = document.querySelectorAll("div[data-nc]"); //no wasted space on large screens (old limit 1024px)
it.forEach(el=>{el.parentNode.style.maxWidth = "unset";});
if (settings.removeSiteNotes && cont != null){
let adel=cont.querySelector("section");
if(adel!=null)
adel.style.display = "none"; //hide advertisement on top of left bar
}
}, 1000);
}
/**
* 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} 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 (hidespan) {
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} */
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} */
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
}
}
/** Helper function checks overlap between 2 rectangles
// * requires top, height, left and width
// * @param {DOMRect} a first rectangle
// * @param {DOMRect} b second rectangle
// * @param {Number} ov ignored overlap margin
// * @returns boolean wether both rectangles have an overlap
// */
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() {
if (!settings.dragSelect) return;
//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 (ev.target.closest("#dA_FN_settings") != 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 closeSettingsForm() {
settings.filterSide = document.getElementById("dA_FN_FilterSide").checked;
settings.dragSelect = document.getElementById("dA_FN_DragSelect").checked;
settings.updateCss = document.getElementById("dA_FN_UpdateCSS").checked;
settings.useCustomCss = document.getElementById("dA_FN_useCustomCSS").checked;
settings.customCss = document.getElementById("dA_FN_customCSS").value;
settings.removeSiteNotes=document.getElementById("dA_FN_RemSiteNotes").checked;
saveSettings();
settingForm.style.display = "none";
location.reload();
}
function showSettingsForm() {
if (document.getElementById("dA_FN_settings") == null) {
settingForm = document.createElement("div");
settingForm.id = "dA_FN_settings";
settingForm.innerHTML = SettingFormTmpl;
document.body.append(settingForm);
document.getElementById("dA_FN_OKBtn").addEventListener("click", closeSettingsForm, false);
document.getElementById("dA_FN_cancelBtn").addEventListener("click", () => {
settingForm.style.display = "none";
}, false);
}
if (settings.customCss == "") settings.customCss = CSSInsertTMPL.replace(/^\t*(.*?)\n/gim, "$1\n");
document.getElementById("dA_FN_FilterSide").checked = settings.filterSide ? "checked" : "";
document.getElementById("dA_FN_DragSelect").checked = settings.dragSelect ? "checked" : "";
document.getElementById("dA_FN_UpdateCSS").checked = settings.updateCss ? "checked" : "";
document.getElementById("dA_FN_useCustomCSS").checked = settings.useCustomCss ? "checked" : "";
document.getElementById("dA_FN_customCSS").value = settings.customCss;
document.getElementById("dA_FN_RemSiteNotes").checked = settings.removeSiteNotes ? "checked" : "";
settingForm.style.display = "";
}
//** save all Settings as json strings */
function saveSettings() {
GM.setValue("filter", JSON.stringify(filter));
GM.setValue("hidden", JSON.stringify(Array.from(hideBuckets)));
GM.setValue("settings", JSON.stringify(settings));
}
/** Load user settings and initializes variables */
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);
return GM.getValue("settings", null)
}).then(ret => {
let pr = JSON.parse(ret);
if (pr != null) settings = pr;
});
}
/**
* inserts GUI for filter bar on the right side.
* if deactivated, empty div is created to mark page as evaluated
*/
function insertSideBarFilter() {
if (!settings.filterSide) {
let filEl = document.createElement("div");
filEl.id = "dA_FN_bar";
filEl.style.display = "none";
document.body.append(filEl);
return;
}
//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);
cont.insertBefore(bar, cont.querySelector(`.${itemClass}`)); //insert container
bar.nextSibling.style.top = "85px";
}
/** 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, document).iterateNext();
if (xEl == null) return; //no place to add filter
itemClass = xEl.className;
cont = document.querySelector(`.${itemClass}`).parentElement;
let setBtn = document.createElement("div");
setBtn.id = "dA_FN_settingBtn";
setBtn.innerHTML = ImgGear;
setBtn.addEventListener("click", showSettingsForm, false);
cont.closest("section").append(setBtn);
insertSideBarFilter();
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);
})();