Remove topics by flair. Only works with Classic Reddit
// ==UserScript== // @name Reddit Flair Ban // @namespace RedditUsercript // @version 1 // @description Remove topics by flair. Only works with Classic Reddit // @author John Doe // @match https://www.reddit.com/r/* // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.serializeJSON/2.9.0/jquery.serializejson.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/alasql/0.5.1/alasql.min.js // @require https://greasyfork.org/scripts/390426-bulma-css-framework-0-7-5/code/bulma-css-framework-075.js?version=735315 // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== /* global $:false, jQuery:false, debug:false, alasql:false, Noty:false */ ((() => { const VERSION = 1; const DEBUG = 0; const cssFiles = [ "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/fontawesome.min.css", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/solid.min.css", "https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.min.css" ]; const htmlStyles = ` <style type="text/css"> .is-clipped { overflow: hidden !important; } #flair_list { font-size: 12px; font-weight: bold; } /* ################################ */ .layout-css-ban-flair-icon { font-size: 10px; color: black; cursor:pointer; padding-bottom: 2px; } .layout-unban-flair-icon { color:green; cursor:pointer; } .js-banned-flair-list-opener .fa-stack { font-size: 0.7em; } .js-banned-flair-list-opener i { vertical-align: middle; } </style> `; // <i class="fas fa-cogs"></i> const htmlCPbutton = ` <span> <button class="js-banned-flair-list-opener"> <span class="fa-stack fa-2x"> <i class="fas fa-hammer fa-stack-1x"></i> <i class="fas fa-ban fa-stack-2x" style="color:Tomato"></i> </span> </button> <span class="separator">|</span> </span> `; const htmlPageAppend = ` <div class="bulma"> <!-- Banned Flair List --> <div id="id-card-banned-flairs" class="modal"> <div class="modal-background"></div> <div class="modal-card"> <header class="modal-card-head"> <p class="modal-card-title"> <span class="js-uscr-modal-draggable-handler cursor--move">Banned Flairs</span> </p> <button class="delete js-uscr-card-button-close" aria-label="close"></button> </header> <form id="id-form-banned-flairs"> <section class="modal-card-body"> <!-- Content ... --> <div class="columns"> <div class="column"> The following flairs are banned. To unban click <i class="fas fa-plus-circle"></i></span> icon. </div> </div> <div class="columns"> <div class="column"> <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth" id="flair_list"> </table> </div> </div> </section> <footer class="modal-card-foot"> <button class="button is-primary js-uscr-card-button-close">Close</button> </footer> </form> </div> </div> <!-- /CP --> </div> `; // functions let storage = { version: 1, // compress: false, options: { prefix: '' }, // Greasemonkey storage API read: function (key, defaultValue) { const raw = GM_getValue(this._prefix(key), defaultValue); // let str = (this.compress) ? LZString.decompressFromUTF16(raw) : raw; return this._parse(raw); }, write: function (key, value) { const raw = this._stringify(value); // let str = (this.compress) ? LZString.compressToUTF16(raw) : raw; return GM_setValue(this._prefix(key), raw); }, delete: function (key) { return GM_deleteValue(this._prefix(key)); }, readKeys: function () { return GM_listValues(); }, // browser localstorage // read: function (key, defaultValue) { // const raw = localStorage.getItem(this._prefix(key), defaultValue); // const val = raw || defaultValue; // // const str = (this.compress) ? LZString.decompressFromUTF16(val) : val; // // return this._parse(str); // return this._parse(val); // }, // write: function (key, value) { // const raw = this._stringify(value); // // let str = (this.compress) ? LZString.compressToUTF16(raw) : raw; // localStorage.setItem(this._prefix(key), raw); // return; // }, // delete: function (key) { // return localStorage.removeItem(this._prefix(key)); // }, // readKeys: function () { // let keys = []; // for(let i=0, l=localStorage.length; i < l; i++){ // keys.push( localStorage.getItem(localStorage.key(i)) ); // } // return keys; // }, // "Set" means "add if absent, replace if present." set: function (key, value) { let savedVal = this.read(key); if (typeof savedVal === 'undefined' || !savedVal) { // add if absent return this.add(key, value); } else { // replace if present this.write(key, value); return true; } }, // "Add" means "add if absent, do nothing if present" (if a uniquing collection). add: function (key, value) { let savedVal = this.read(key, false); if (typeof savedVal === 'undefined' || !savedVal) { this.write(key, value); return true; } else { if (this._isArray(savedVal)) { // is array let index = savedVal.indexOf(value); if (index !== -1) { // do nothing if present return false; } else { // add if absent savedVal.push(value); this.write(key, savedVal); return true; } } else if (this._isObject(savedVal)) { // is object // merge obj value on obj let r###lt, objToMerge = value; r###lt = Object.assign(savedVal, objToMerge); this.write(key, r###lt); return false; } return false; } }, // "Replace" means "replace if present, do nothing if absent." replace: function (key, itemFind, itemReplacement) { let savedVal = this.read(key, false); if (typeof savedVal === 'undefined' || !savedVal) { // do nothing if absent return false; } else { if (this._isArray(savedVal)) { // is Array let index = savedVal.indexOf(itemFind); if (index !== -1) { // replace if present savedVal[index] = itemReplacement; this.write(key, savedVal); return true; } else { // do nothing if absent return false; } } else if (this._isObject(savedVal)) { // is Object // replace property's value savedVal[itemFind] = itemReplacement; this.write(key, savedVal); return true; } return false; } }, // "Remove" means "remove if present, do nothing if absent." remove: function (key, value) { if (typeof value === 'undefined') { // remove key this.delete(key); return true; } else { // value present let savedVal = this.read(key); if (typeof savedVal === 'undefined' || !savedVal) { return true; } else { if (this._isArray(savedVal)) { // is Array let index = savedVal.indexOf(value); if (index !== -1) { // remove if present savedVal.splice(index, 1); this.write(key, savedVal); return true; } else { // do nothing if absent return false; } } else if (this._isObject(savedVal)) { // is Object let property = value; delete savedVal[property]; this.write(key, savedVal); return true; } return false; } } }, get: function (key, defaultValue) { return this.read(key, defaultValue); }, getAll: function () { const keys = this._listKeys(); let obj = {}; for (let i = 0, len = keys.length; i < len; i++) { obj[keys[i]] = this.read(keys[i]); } return obj; }, getKeys: function () { return this._listKeys(); }, getPrefix: function () { return this.options.prefix; }, empty: function () { const keys = this._listKeys(); for (let i = 0, len = keys.lenght; i < len; i++) { this.delete(keys[i]); } }, has: function (key) { return this.get(key) !== null; }, forEach: function (callbackFunc) { const allContent = this.getAll(); for (let prop in allContent) { callbackFunc(prop, allContent[prop]); } }, _parse: function (value) { if (this._isJson(value)) { return JSON.parse(value); } return value; }, _stringify: function (value) { if (this._isJson(value)) { return value; } return JSON.stringify(value); }, _listKeys: function (usePrefix = false) { const prefixed = this.readKeys(); let unprefixed = []; if (usePrefix) { return prefixed; } else { for (let i = 0, len = prefixed.length; i < len; i++) { unprefixed[i] = this._unprefix(prefixed[i]); } return unprefixed; } }, _prefix: function (key) { return this.options.prefix + key; }, _unprefix: function (key) { return key.substring(this.options.prefix.length); }, _isJson: function (item) { try { JSON.parse(item); } catch (e) { return false; } return true; }, _isObject: function (a) { return (!!a) && (a.constructor === Object); }, _isArray: function (a) { return (!!a) && (a.constructor === Array); } }; function isObject(val) { if (val === null) { return false; } return ((typeof val === "function") || (typeof val === "object")); } function setDebug(isDebug = false) { if (isDebug) { window.debug = window.console.log.bind(window.console, "%s: %s"); } else { window.debug = function () {}; window.console.log = function () {}; } } function appendFilesToHead(arr = [], forceExt = false) { for (let i = 0; i < arr.length; i++) { let urlStr = arr[i]; let ext = (forceExt) ? forceExt : urlStr.slice((Math.max(0, urlStr.lastIndexOf(".")) || Infinity) + 1); let ele = null; switch (ext) { case "js": ele = document.createElement("script"); ele.type = "text/javascript"; ele.src = urlStr; break; case "css": ele = document.createElement("link"); ele.rel = "stylesheet"; ele.type = "text/css"; ele.href = urlStr; break; default: ele = document.createElement("script"); ele.type = "text/javascript"; ele.src = urlStr; } document.getElementsByTagName("head")[0].appendChild(ele); } } function onlyUnique(value, index, self) { return self.indexOf(value) === index; } function decodeHtml(html) { let txt = document.createElement("textarea"); txt.innerHTML = html; return txt.value; } function db_init() { CURRENT_LOCALSTORAGE_DB = storage.get("flairs", []); mybase = new alasql.Database("mybase"); mybase.exec("CREATE TABLE flairs (subname STRING, flair STRING)"); debug("localstorage db.lenght", CURRENT_LOCALSTORAGE_DB.length); if (CURRENT_LOCALSTORAGE_DB.length >= 1) { alasql.databases.mybase.tables.flairs.data = CURRENT_LOCALSTORAGE_DB; } } function save_db() { console.log(alasql.databases.mybase.tables.flairs.data); storage.set("flairs", alasql.databases.mybase.tables.flairs.data); } // https://stackoverflow.com/questions/7298364/using-jquery-and-json-to-populate-forms function populateForm($form, data) { $.each(data, (key, value) => { // all json fields ordered by name let $ctrls, $ctrl; if (value instanceof Array) { $ctrls = $form.find("[name='" + key + "[]']"); //all form elements for a name. Multiple checkboxes can have the same name, but different values } else { $ctrls = $form.find("[name='" + key + "']"); } if ($ctrls.is("select")) { //special form types $("option", $ctrls).each(function () { if (this.value == value) { this.selected = true; } }); } else if ($ctrls.is("textarea")) { $ctrls.val(value); } else { switch ($ctrls.attr("type")) { //input type case "text": case "hidden": $ctrls.val(value); break; case "radio": if ($ctrls.length >= 1) { $.each($ctrls, function (index) { // every individual element let elemValue = $(this).attr("value"); let singleVal = value; let elemValueInData = singleVal; if (elemValue == value) { $(this).prop("checked", true); } else { $(this).prop("checked", false); } }); } break; case "checkbox": if ($ctrls.length > 1) { $.each($ctrls, function (index) { // every individual element let elemValue = $(this).attr("value"); let elemValueInData; let singleVal; for (let i = 0; i < value.length; i++) { singleVal = value[i]; debug("singleVal", singleVal, "/value[i][1]", value[i][1]); if (singleVal == elemValue) { elemValueInData = singleVal; } } if (elemValueInData) { $(this).prop("checked", true); } else { $(this).prop("checked", false); } }); } else if ($ctrls.length == 1) { $ctrl = $ctrls; if (value) { $ctrl.prop("checked", true); } else { $ctrl.prop("checked", false); } } break; } //switch input type } // if/else }); // all json fields } // populate form function getSubName() { let url = window.location.href; let regex = /^https?:\/\/(?:www\.)?reddit\.com\/r\/([^\/?\s]+)\/?/i; let match = url.match(regex); return match[1]; } function getSubBannedFlairs() { return mybase.exec("SELECT * FROM flairs WHERE subname=?", [CURRENT_SUB_NAME]); } // end functions setDebug(DEBUG); let mybase = null; let CURRENT_SUB_NAME = null; let CURRENT_SUB_FLAIRS = []; let CURRENT_LOCALSTORAGE_DB = null; let CURRENT_USERNAME = $("#header .user A").text(); db_init(); CURRENT_SUB_NAME = getSubName(); CURRENT_SUB_FLAIRS = getSubBannedFlairs(); console.log('CURRENT_SUB_NAME', CURRENT_SUB_NAME); console.log('CURRENT_SUB_FLAIRS', CURRENT_SUB_FLAIRS); appendFilesToHead(cssFiles, "css"); $("head").append(htmlStyles); $("body").append(htmlPageAppend); $("#header-bottom-right").prepend(htmlCPbutton); setTimeout(function(){ let c = 0; CURRENT_SUB_FLAIRS.forEach(function(item){ $(`.linkflairlabel[title="${item.flair}"]`).each(function(){ console.log('remove thread', item.flair); $(this).closest("DIV[data-fullname^='t3_']").remove(); c++; }); }); console.log('counter : removed threads', c ); $("p.title > .linkflairlabel ").each(function (index, value) { let html = ""; let name = $(this).text(); html = `<span class="js-flair-ban" data-name="${name}" title="Filter : Remove '${name}' from listing"><span class="layout-css-ban-flair-icon"><i class="fas fa-minus-circle"></i></span></span>`; let append = `<span class="layout-after-flair" data-name="${name}">${html} </span>`; $(this).after(append); }); // CARD > BUTTON CLOSE $(".js-uscr-card-button-close").click(function () { event.preventDefault(); let card_id = $(this).closest(".modal").attr("id"); $(`#${card_id}`).removeClass('is-active'); $('html').removeClass('is-clipped'); return false; }); // OPEN $(".js-banned-flair-list-opener").click(function () { let html_flairs = ''; let r###lt = mybase.exec("SELECT * FROM flairs WHERE subname = ?", [CURRENT_SUB_NAME]); let counter = 0; console.log(r###lt); r###lt.forEach(function(item){ counter++; html_flairs += ` <tr id="row_${counter}"> <td>${item.flair} </td> <td><span class="js-flair-unban layout-unban-flair-icon" data-name="${item.flair}" data-row="${counter}"><i class="fas fa-plus-circle"></i></span></td> </tr> `; }); $("#flair_list").html(html_flairs); $('html').addClass('is-clipped'); $("#id-card-banned-flairs").addClass("is-active"); }); $(".js-flair-ban").click(function () { let name = $(this).attr("data-name"); console.log('name', name); mybase.exec("DELETE FROM flairs WHERE subname=? and flair = ?", [CURRENT_SUB_NAME, name]); mybase.exec("INSERT INTO flairs (?,?)", [CURRENT_SUB_NAME, name]); new Noty({ type: 'success', text: 'Flair banned: ' + name + '', }).show(); save_db(); }); $(document).on('click', '.js-flair-unban', function(e) { let name = $(this).attr("data-name"); let row = $(this).attr("data-row"); console.log('name', name); mybase.exec("DELETE FROM flairs WHERE subname=? and flair = ?", [CURRENT_SUB_NAME, name]); $("#row_"+row).remove(); new Noty({ type: 'success', text: 'Flair unbanned: ' + name + '', }).show(); save_db(); }); }, 200); Noty.overrideDefaults({ layout : 'topRight', closeWith: ['click', 'button'], progressBar: false, timeout: 2000, closeWith: ['click'], }); }))();