Change font, size, width and background of a work + blacklist: hide works that contain certains tags or text, have too many tags/fandoms/relations/chapters/words and other options + fullscreen reading mode + bookmarks: save the position you stopped reading a fic + number of words for each chapter and estimated reading time
// ==UserScript==// @name AO3: Fic's Style, Blacklist, Bookmarks// @namespace https://github.com/Schegge// @description Change font, size, width and background of a work + blacklist: hide works that contain certains tags or text, have too many tags/fandoms/relations/chapters/words and other options + fullscreen reading mode + bookmarks: save the position you stopped reading a fic + number of words for each chapter and estimated reading time// @icon https://raw.githubusercontent.com/Schegge/Userscripts/master/images/ao3icon.png// @version 3.6.1.4// @author Schegge// @match *://archiveofourown.org/*// @match *://www.archiveofourown.org/*// @grant GM_getValue// @grant GM_setValue// @grant GM.getValue// @grant GM.setValue// ==/UserScript==// gm4 polyfill https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.jsif (typeof GM == 'undefined') {this.GM = {};Object.entries({'GM_getValue': 'getValue','GM_setValue': 'setValue'}).forEach(([oldKey, newKey]) => {let old = this[oldKey];if (old && (typeof GM[newKey] == 'undefined')) {GM[newKey] = function(...args) {return new Promise((resolve, reject) => {try { resolve(old.apply(this, args)); } catch (e) { reject(e); }});};}});}(async function() {const SN = 'stblbm';// check which pageconst Check = {// script versionversion: async function() {if (await getStorage('version', '1') !== 3614) {setStorage('version', 3614);return true;}return false;},// on search pages but not on personal user profileblack: function() {let user = document.querySelector('#greeting .user a[href*="/users/"]') || false;user = user && window.location.pathname.includes(user.href.split('/users/')[1]);return document.querySelector('li.blurb.group:not(.collection):not(.tagset)') && !user;},// include /works/(numbers) and /works/(numbers)/chapters/(numbers)// and exclude /works/(whatever)navigatework: function() {return /\/works\/\d+(\/chapters\/\d+)?(?!.*navigate)/.test(window.location.pathname);},// Full ScreenfullScreen: false};// new version checkif (await Check.version()) {/*document.body.insertAdjacentHTML('beforeend',`<div style="position: fixed; bottom: 3em; right: 3em; width: 35%; z-index: 999;font-size: .9em; background: #fff; padding: 1em; border: 1px solid #900;"><b>AO3: Fic's Style, Blacklist, Bookmarks</b> UPDATES (v3.6.1.3)<br><br><br><br><span id="${SN}-close" style="cursor: pointer; color: #900;">close</span></div>`);document.getElementById(`${SN}-close`).addEventListener('click', function() {this.parentElement.style.display = 'none';});*/}/** FEATURES **/const Feature = {style: true,book: true,black: true,wpm: 250};Object.assign(Feature, await getStorage('feature', '{}'));// Features' menuaddCSS(`${SN}-menus`,`li[id|="${SN}"] a { cursor: pointer; }li[id|="${SN}"] .dropdown-menu li a.${SN}-save {color: #900!important; font-weight: bold; text-align: center;padding-bottom: 0.75em!important; }li[id|="${SN}"] .dropdown-menu input[type="number"],li[id |= "${SN}"] .dropdown-menu input[type="text"] {width: 3.5em; padding: 0 0 0 .2em; margin: 0; background: #fff; }li[id|="${SN}"] .dropdown-menu input[type="checkbox"] { margin: 0; }li[id|="${SN}"] .dropdown-menu textarea {font-size: .9em; line-height: 1.2em; min-height: 4em; padding: .3em;margin: .1em .5em; width: calc(100% - 1em); box-sizing: border-box; resize: vertical; }li[id|="${SN}"] .${SN}-opts {display: flex!important; flex-wrap: nowrap; align-items: center; }li[id|="${SN}"] .${SN}-opts span {width: 25%; flex: auto; font-size: .75em; text-transform: uppercase; padding: .3em 0; }#${SN}-black .dropdown-menu { width: 28em; }#${SN}-black .dropdown-menu .${SN}-opts { text-align: center!important; }#${SN}-black .dropdown-menu input[type="text"] { width: 8em; }#${SN}-book .${SN}-opts a:first-child { flex-grow: 1; font-size: .9em; }a.${SN}-book-delete { color: #900!important; }div[class*="${SN}-book"] a { margin: 1em .2em 0 0; font-size: .8em; cursor: pointer; }.${SN}-book-left {position: fixed; left: 0; bottom: 0; margin: 0 0 .8em .5em; z-index: 999; }.${SN}-book-top { text-align: right;}.${SN}-no-book { display: none!important; }#${SN}-style {position: fixed; bottom: 0; right: 0; margin: 0 .5em .8em 0; padding: 0;border-radius: .3em; background-color: transparent; text-align: right;font-size: .9em; z-index: 999; }#${SN}-style:not(.${SN}-style-hide) { width: 25em; }#${SN}-style.${SN}-style-hide > div { display: none; }#${SN}-style > div {color: #000; background-color: #ddd; padding: 0 .5em; box-shadow: 1px 1px 3px -1px #444;margin: 0; border-radius: .2em; }#${SN}-style label {display: block; border-bottom: 1px solid #888; padding: .2em 0; margin: 0; }#${SN}-style input, #${SN}-style select {width: 50%; padding: 0; margin: 0 0 0 1em; vertical-align: middle; }#${SN}-style button { margin: .3em .2em; }#${SN}-style-button { padding: 0 .3em; }.${SN}-words {font-size: .7em; color: inherit; font-family: consolas, monospace;text-transform: uppercase; text-align: center; margin: 3em 0 .5em; }`);let featureMenu = document.createElement('li');featureMenu.id = `${SN}-feature`;featureMenu.className = 'dropdown';featureMenu.innerHTML = `<a style="font-weight: bold;">Features</a><ul class="menu dropdown-menu"><li><a><input id="${SN}-feature-style" type="checkbox" ${Feature.style ? 'checked' : ''}> Styling</a></li><li><a><input id="${SN}-feature-book" type="checkbox" ${Feature.book ? 'checked' : ''}> Bookmarks / Full Screen</a></li><li><a><input id="${SN}-feature-black" type="checkbox" ${Feature.black ? 'checked' : ''}> Blacklist</a></li><li><a><input id="${SN}-feature-wpm" type="number" min="0" max="1000" step="10" value="${Feature.wpm}"> Words per minute</a></li><li><a class="${SN}-save" id="${SN}-feature-save">SAVE</a></li></ul>`;document.querySelector('#header ul.primary.navigation.actions').appendChild(featureMenu);document.getElementById(`${SN}-feature-save`).addEventListener('click', function() {Feature.style = document.getElementById(`${SN}-feature-style`).checked;Feature.book = document.getElementById(`${SN}-feature-book`).checked;Feature.black = document.getElementById(`${SN}-feature-black`).checked;let wpm = document.getElementById(`${SN}-feature-wpm`).value.trim();Feature.wpm = wpm ? Math.min(Math.max(parseInt(wpm), 0), 1000) : 0;setStorage('feature', Feature);this.textContent = 'SAVING...';window.location.replace(window.location.href);});// add estimated reading time for every fic foundif (Feature.wpm) {for (let work of document.querySelectorAll('dl.stats dd.words')) {let numWords = work.textContent.replace(/,/g, '');work.insertAdjacentHTML('afterend', `<dt>Time:</dt><dd>${countTime(numWords)}</dd>`);}}/** BOOKMARKS **/if (Feature.book) {const Bookmarks = {list: [],getValues: async function() {this.list = await getStorage('bookmarks', '[]');},setValues: function() {setStorage('bookmarks', this.list);},fromBook: window.location.search === '?bookmark',getUrl: window.location.pathname.split('/works/')[1],getTitle: function() {let title = document.querySelector('#workskin .preface.group h2.title.heading').textContent.trim().substring(0, 28);// get the number of the chapter if chapter by chapterif (this.getUrl.includes('/chapters/')) {title += ` (${document.querySelector('#chapters > .chapter > .chapter.preface.group > h3 > a').textContent.replace('Chapter ', 'ch')})`;}return title;},getPosition: function() {let position = getScroll();// calculate % if chapter by chapter view or work completed (number/number is the same)if (window.location.pathname.includes('/chapters/') ||/(\d+)\/\1/.test(document.querySelector('dl.stats dd.chapters').textContent)) {position = (position / getDocHeight()).toFixed(4) + '%';}return position;},checkIfExist: function(what, link) {let url = link || this.getUrl;let found = false;for (let [index, bookmark] of this.list.entries()) {// check if the same fic already existsif (bookmark[0].split('/chapters/')[0] !== url.split('/chapters/')[0]) {continue;}// i need the index to delete the old bookmark (for change or delete)if (what === 'cancel') {found = index;break;// check if the same chapter} else if (bookmark[0] === url) {// retrieve the bookmark positionif (what === 'book') {found = bookmark[2];// if the bookmark is in %if (found.toString().includes('%')) {found = parseFloat(found.replace('%', '')) * getDocHeight();}} else {// just check if a bookmark existfound = true;}break;}}return found;},cancel: function(url) {let found = this.checkIfExist('cancel', url);// !== false because it can return 0 for the indexif (found !== false) this.list.splice(found, 1);},getNew: function() {this.cancel();this.list.push([this.getUrl, this.getTitle(), this.getPosition()]);this.setValues();},html: function() {let bookMenu = document.createElement('li');bookMenu.id = `${SN}-book`;bookMenu.className = 'dropdown';bookMenu.innerHTML = '<a>Bookmarks</a>';let bookMenuDrop = document.createElement('ul');bookMenuDrop.className = 'menu dropdown-menu';bookMenu.appendChild(bookMenuDrop);document.querySelector('#header ul.primary.navigation.actions').appendChild(bookMenu);if (this.list.length) {let self = this;let clickDelete = function() {self.cancel(this.getAttribute('data-url'));self.setValues();this.style.display = 'none';this.previousSibling.style.opacity = '.4';};for (let item of this.list) {let bookMenuLi = document.createElement('li');bookMenuLi.className = `${SN}-opts`;bookMenuLi.innerHTML = `<a href="https://archiveofourown.org/works/${item[0]}?bookmark">${item[1]}</a>`;let bookMenuDelete = document.createElement('a');bookMenuDelete.className = `${SN}-book-delete`;bookMenuDelete.title = 'delete bookmark';bookMenuDelete.setAttribute('data-url', item[0]);bookMenuDelete.textContent = 'x';bookMenuDelete.addEventListener('click', clickDelete);bookMenuLi.appendChild(bookMenuDelete);bookMenuDrop.appendChild(bookMenuLi);}} else {bookMenuDrop.innerHTML = '<li><a>No bookmark yet.</a></li>';}}};await Bookmarks.getValues();Bookmarks.html();// Fullscreenif (Check.work()) {let workskin = document.getElementById('workskin');let ficTop = document.createElement('div');ficTop.className = `actions ${SN}-book-top`;let toFullScreen = document.createElement('a');toFullScreen.textContent = 'Full Screen';ficTop.appendChild(toFullScreen);workskin.insertAdjacentElement('afterbegin', ficTop);// changes to create full screenlet fullScreen = () => {if (Check.fullScreen) {window.location.replace(window.location.pathname);return;}setScroll(0);Check.fullScreen = true;window.history.replaceState(null, '', '?bookmark');addCSS(`${SN}-fullscreen`,`#outer.wrapper, div#outer.wrapper > * { display: none!important; }#workskin .preface { margin: 0; padding-bottom: 0; }div.preface .notes, div.preface .summary,div.preface .series, div.preface .children { min-height: 0; }div.preface .module { padding-bottom: 0; text-align: center; }.preface .module h3.heading {display: inline; cursor: pointer; text-align: center; opacity: .5;font-style: italic; font-size: 100%; }.preface .module > :not(h3) { display: none; }.preface h3 + p {border: 3px solid rgba(0, 0, 0, .1); border-left: 0; border-right: 0;padding: .6em; margin: 0; }.preface .module > h3:hover ~ .userstuff, .preface .module > .userstuff:hover,.preface .module > h3:hover ~ ul, .preface .module > ul:hover,.preface .module > h3:hover + p, .preface .module > h3 + p:hover {display: block!important; position: absolute; width: 100%; max-height: 6em;font-size: .8em; transform: translateY(-100%); color: rgb(42, 42, 42);background-color: #fff; padding: 10px; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .4);margin: 0; overflow: auto; z-index: 999; cursor: pointer; }.actions:not(div[class*="${SN}-book"]) li > a:not([href*="chapters"]):not([href="#workskin"]) {display: none; }.actions:not(div[class*="${SN}-book"]) { margin-top: 2em; }`);document.body.appendChild(workskin);toFullScreen.textContent = 'Exit';let goToBook = document.createElement('a');goToBook.textContent = 'Go to Bookmark';goToBook.addEventListener('click', () => {setScroll(Bookmarks.checkIfExist('book'));});let ficLeft = document.createElement('div');ficLeft.className = `actions ${SN}-book-left`;let deleteBook = document.createElement('a');deleteBook.title = 'delete bookmark';deleteBook.textContent = 'x';deleteBook.addEventListener('click', () => {Bookmarks.cancel();Bookmarks.setValues();goToBook.className = `${SN}-no-book`;deleteBook.className = `${SN}-no-book`;});let newBook = document.createElement('a');newBook.title = 'new bookmark';newBook.textContent = '+';newBook.addEventListener('click', function() {Bookmarks.getNew();goToBook.className = '';deleteBook.className = '';this.textContent = 'saved';setTimeout(() => { this.textContent = '+'; }, 1000);});if (!Bookmarks.checkIfExist()) {goToBook.className = `${SN}-no-book`;deleteBook.className = `${SN}-no-book`;}ficTop.insertAdjacentElement('afterbegin', goToBook);ficLeft.appendChild(newBook);ficLeft.appendChild(deleteBook);document.body.appendChild(ficLeft);document.querySelector('#feedback .actions a[href="#main"]').href = '#workskin';workskin.appendChild(document.querySelector('#feedback .actions'));};if (Bookmarks.fromBook) fullScreen();toFullScreen.addEventListener('click', fullScreen);} // END Check.work()} // END Feature.book/** FIC'S STYLE + WPM **/if (Check.work()) {if (Feature.style) {addCSS(`${SN}-generalstyle`,`#main div.wrapper { margin-bottom: 1em; }#workskin { margin: 0; max-width: none!important; }#workskin .notes, #workskin .summary, blockquote {font-size: inherit; font-family: inherit; }.preface a, #chapters a, .preface a:link, #chapters a:link, .preface a:visited,#chapters a:visited, .preface a:visited:hover, #chapters a:visited:hover {color: inherit !important; }.actions {font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif;font-size: 14px; }.chapter .preface { border-top: 0; margin-bottom: 0; padding: 0 2em; }.chapter .preface[role="complementary"] { border-width: 0; margin: 0; }.preface.group, div.preface {color: inherit; background-color: inherit; margin-left: 0; margin-right: 0;padding: 0 2em; }#workskin #chapters .preface .userstuff p, #workskin .preface .userstuff p {margin: .1em auto; line-height: 1.1em; }div.preface .byline a, #workskin #chapters a, #chapters a:link, #chapters a:visited {color: inherit; }div.preface .notes, div.preface .summary, div.preface .series, div.preface .children {min-height: 0; }div.preface .jump { margin-top: 1em; font-size: .9em; }.preface blockquote {box-shadow: 0 0 0 2px rgba(0, 0, 0, .1), 0 0 0 2px rgba(255, 255, 255, .2);padding: .6em; margin: 0; }.preface h3.title {background: repeating-linear-gradient(45deg, rgba(0, 0, 0, .05), rgba(0, 0, 0, .1) 2px,rgba(255, 255, 255, .2) 2px, rgba(255, 255, 255, .2) 4px);padding: .6em; margin: 0; }.preface h3.heading { font-size: inherit; border-width: 0; }h3.title a { border: 0; font-style: italic; }div.preface .associations, .preface .notes h3+p {margin-bottom: 0; font-style: italic; font-size: .8em; }#workskin #chapters, #workskin #chapters .userstuff {width: 100%!important; box-sizing: border-box; }#workskin #chapters .userstuff, #workskin #chapters .userstuff p {font-family: inherit; }#workskin #chapters .userstuff br { display: block; margin-top: .6em; content: " "; }.userstuff hr {width: 100%; height: 2px; border: 0; margin: 1.5em 0;background-image: linear-gradient(90deg, transparent, rgba(0, 0, 0, .2), transparent),linear-gradient(90deg, transparent, rgba(255, 255, 255, .3), transparent); }#workskin #chapters .userstuff blockquote {padding-top: 1px; padding-bottom: 1px; margin: 0 .5em; font-size: inherit; }.userstuff img {max-width: 100%; height: auto; display: block; margin: auto; }`);// CSS changes depending on the userconst Styling = {opts: {fontName: 'Default',colors: 'light',textAlign: 'justify',fontSize: '100',margins: '7',lineSpacing: '5'},inputs: [// 0:id, 1:name, 2+:options['fontName', 'Font', 'Default', 'Arial Black', 'Helvetica', 'Verdana', 'Segoe UI','Garamond', 'Georgia', 'Times New Roman', 'Consolas', 'Courier'],['colors', 'Background', 'light', 'grey', 'sepia', 'dark', 'darkblue', 'black'],['textAlign', 'Alignment', 'default', 'justify', 'left', 'center', 'right'],['fontSize', 'Text Size', 100, 50, 300],['margins', 'Page Margins', 7, 5, 40],['lineSpacing', 'Line Spacing', 5, 3, 10]],fonts: {'inherit': 'inherit', // old default'Default': 'inherit','Arial Black': 'Arial Black, Arial Bold, Gadget, sans-serif','Helvetica': 'Helvetica, Helvetica Neue, sans-serif','Verdana': 'Verdana, Tahoma, sans-serif','Segoe UI': 'Segoe UI, Trebuchet MS, sans-serif','Garamond': 'Garamond, Book Antiqua, Palatino, Baskerville, serif','Georgia': 'Georgia, serif','Times New Roman': 'Times New Roman, Times, serif','Consolas': 'Consolas, Lucida Console, monospace','Courier': 'Courier, Courier New, monospace'},colors: {// background, font colorlight: ['#ffffff', '#000000'],grey: ['#e6e6e6', '#111111'],sepia: ['#fbf0d9', '#54331b'],dark: ['#333333', '#e1e1e1'],darkblue: ['#282a36', '#f8f8e6'],black: ['#000000', '#ffffff']},getValues: async function() {Object.assign(this.opts, await getStorage('styling', '{}'));},setValues: function() {setStorage('styling', this.opts);addCSS(`${SN}-userstyle`,`#workskin {font-family: ${this.fonts[this.opts.fontName]};font-size: ${this.opts.fontSize/100}em;padding: 0 ${this.opts.margins}%;color: ${this.colors[this.opts.colors][1]};background-color: ${this.colors[this.opts.colors][0]};${this.opts.textAlign === 'default' ? '' :`text-align: ${this.opts.textAlign};`}}#workskin #chapters .userstuff {line-height: ${this.opts.lineSpacing * 0.3}em;${this.opts.textAlign === 'default' ? '' :`text-align: ${this.opts.textAlign};`}}#workskin #chapters .userstuff p {line-height: ${this.opts.lineSpacing * 0.3}em;margin: ${this.opts.lineSpacing * 0.5 - 1.4}em auto;${this.opts.textAlign === 'default' ? '' :`text-align: ${this.opts.textAlign};`}}`);},html: function() {this.setValues();// the options displayed on the pagelet styleMenu = document.createElement('div');styleMenu.id = `${SN}-style`;styleMenu.className = `${SN}-style-hide`;let h = '';for (let input of this.inputs) {h += `<label>${input[1]}`;if (typeof input[2] === 'string') {h += `<select id="${input[0]}">`;for (let i = 2; i < input.length; i++) {h += `<option value="${input[i]}" ${input[i] === this.opts[input[0]] ? 'selected' : ''}>${input[i]}</option>`;}h += '</select>';} else {h += `<input type="range" min="${input[3]}" max="${input[4]}" id="${input[0]}" value="${this.opts[input[0]]}">`;}h += '</label>';}styleMenu.innerHTML = `<div>${h}<button id="${SN}-style-save">save</button><button id="${SN}-style-reset">reset</button></div><button id="${SN}-style-button">☰</button>`;document.body.appendChild(styleMenu);document.getElementById(`${SN}-style-save`).addEventListener('click', () => {let pos = getScroll() / getDocHeight();for (let input of this.inputs) {this.opts[input[0]] = styleMenu.querySelector(`#${input[0]}`).value;}this.setValues();setScroll(pos * getDocHeight());});document.getElementById(`${SN}-style-reset`).addEventListener('click', () => {let pos = getScroll() / getDocHeight();styleMenu.parentElement.removeChild(styleMenu);for (let input of this.inputs) {this.opts[input[0]] = input[2];}this.html();setScroll(pos * getDocHeight());});document.getElementById(`${SN}-style-button`).addEventListener('click', function() {this.parentElement.className = this.parentElement.className ? '' : `${SN}-style-hide`;});}};await Styling.getValues();Styling.html();} // END Feature.style// # words and time for every chapter, if the fic has chaptersif (Feature.wpm) {for (let chapter of document.querySelectorAll('#chapters > .chapter > div.userstuff.module')) {// -2 because of hidden <h3>Chapter Text</h3>let numWords = chapter.textContent.replace(/['’‘-]/g, '').match(/\w+/g).length - 2;chapter.parentElement.insertAdjacentHTML('afterbegin',`<div class="${SN}-words">this chapter has ${numWords} words (time: ${countTime(numWords)})</div>`);}}// remove all the non-breaking white spacesdocument.getElementById('chapters').innerHTML = document.getElementById('chapters').innerHTML.replace(/ /g, ' ');} // END Check.work()/** BLACKLIST **/if (Feature.black && Check.black()) {addCSS(`${SN}-blacklisting`,`[data-${SN}-visibility="remove"],[data-${SN}-visibility="hide"] > :not(.header),[data-${SN}-visibility="hide"] > .header > :not(h4) { display: none!important; }[data-${SN}-visibility="hide"] > .header,[data-${SN}-visibility="hide"] > .header > h4 {margin: 0!important; min-height: auto; font-size: .9em; font-style: italic; }[data-${SN}-visibility="hide"] { opacity: .6; }[data-${SN}-visibility="hide"]::before {content: "\\2573 " attr(data-${SN}-reasons); font-size: .8em; }`);const Blacklist = {lists : {Tag: [],Text: [],Author: []},opts: {show: true,pause: false,maxTags: 0,maxFandoms: 0,maxRelations: 0,minIncomplete: 0,minChapters: 0,maxChapters: 0,minWords: 0,maxWords: 0,langs: ''},blurb: 'li.blurb.group',getValues: async function() {this.lists.Tag = await getStorage('blacklistTags', '[]');this.lists.Text = await getStorage('blacklistText', '[]');this.lists.Author = await getStorage('blacklistAuthors', '[]');Object.assign(this.opts, await getStorage('blacklistOpts', '{}'));},findTags: function(work) {return this.opts.maxTags &&work.querySelectorAll('.tag').length > this.opts.maxTags;},findFandoms: function(work) {return this.opts.maxFandoms &&work.querySelectorAll('.header .fandoms .tag').length > this.opts.maxFandoms;},findRelations: function(work) {return this.opts.maxRelations &&work.querySelectorAll('.tags .relationships .tag').length > this.opts.maxRelations;},findLangs: function(work) {return this.opts.langs && work.querySelector('dd.language') &&!this.opts.langs.toLowerCase().includes(work.querySelector('dd.language').textContent.toLowerCase().trim());},getChapters: function(work) {if ((this.opts.minChapters || this.opts.maxChapters) &&work.querySelector('dd.chapters')) {let numCh = Number(work.querySelector('dd.chapters').textContent.split('/')[0]);if (this.opts.minChapters && numCh < this.opts.minChapters ||this.opts.maxChapters && numCh > this.opts.maxChapters) {return `Chapters: ${numCh}`;}}return [];},getWords: function(work) {if ((this.opts.minWords || this.opts.maxWords) && work.querySelector('dd.words')) {let numWords = Number(work.querySelector('dd.words').textContent.replace(/,/g, '')) / 1000;if (this.opts.minWords &&numWords < this.opts.minWords ||this.opts.maxWords &&numWords > this.opts.maxWords) {return `Words: ${Math.round(numWords * 1000)}`;}}return [];},getIncomplete: function(work) {if (this.opts.minIncomplete && work.querySelector('.required-tags .complete-no')) {// millisecs in an average month = 30.4days*24hrs*60mins*60secs*1000let updated = (Date.now() - new Date(work.querySelector('.datetime').textContent).getTime()) / (30.4*24*60*60*1000);if (updated > this.opts.minIncomplete) {return `Updated: ${Math.floor(updated)}mnth ago`;}}return [];},ifMatch: function(elem, list, flag) {let found = false;for (let value of this.lists[list]) {let pattern = value.trim().replace(/[.+?^${}()|[\]\\]/g, '\\$&');if (!pattern) break;// wildcardpattern = pattern.replace(/\*/g, '.*');// match 2 words in any orderpattern = pattern.replace(/(.+)&&(.+)/, '(?=.*$1)(?=.*$2).*');// only otpif (elem.parent === 'relationships') {pattern = pattern.replace(/(.+)&!(.+)/, '(?=.*\\/)((?=.*$1)(?!.*$2)|(?=.*$2)(?!.*$1)).*');}let regex;if (flag === 'free') regex = new RegExp(pattern, 'i'); // for textelse regex = new RegExp(`^${pattern}$`, 'i');if (regex.test(elem.text)) {if (flag === 'free') found = `${list}: ${value}`; // show the rule that matched (for text)else if (elem.parent === 'heading') found = list; // show only list name (for author)else found = `${list}: ${elem.text}`; // show the entire matched tagbreak;}}return found;},getReasons: function(work, list, where, flag = '') {if (!this.lists[list].length) return [];let filtered = [];for (let elem of work.querySelectorAll(where)) {let found = this.ifMatch({text: elem.textContent.trim(),parent: elem.parentElement.className}, list, flag);if (found) filtered.push(found);}return filtered;},setVisibility: function() {if (this.opts.pause) return;for (let work of document.querySelectorAll(this.blurb)) {let reasons = [].concat(this.getReasons(work, 'Author', 'h4.heading a[rel="author"]')).concat(this.getIncomplete(work)).concat(this.getWords(work)).concat(this.getChapters(work)).concat(this.getReasons(work, 'Text', 'h4.heading a:first-child, .summary', 'free')).concat(this.getReasons(work, 'Tag','.tags .tag, .required-tags span:not(.warnings) span.text, .header .fandoms .tag'));if (this.findRelations(work)) reasons.unshift('Relations');if (this.findFandoms(work)) reasons.unshift('Fandoms');if (this.findTags(work)) reasons.unshift('Tags');if (this.findLangs(work)) reasons.unshift('Language');if (!reasons.length) continue;if (this.opts.show) {work.setAttribute(`data-${SN}-visibility`, 'hide');work.setAttribute(`data-${SN}-reasons`, reasons.join(' - '));} else {work.setAttribute(`data-${SN}-visibility`, 'remove');}}},getArray: function(string) {return string.trim() ? string.split(',').filter(s => s.trim().length) : [];},getInt: function(string, min = 0) {let number = string.trim() ? Math.max(parseInt(string), 0) : 0;if (number < min) number = 0;return number;},setValues: function() {this.lists.Tag = this.getArray(document.getElementById(`${SN}-black-tags`).value);this.lists.Text = this.getArray(document.getElementById(`${SN}-black-text`).value);this.lists.Author = this.getArray(document.getElementById(`${SN}-black-authors`).value);this.opts.maxTags = this.getInt(document.getElementById(`${SN}-black-maxTags`).value);this.opts.maxFandoms = this.getInt(document.getElementById(`${SN}-black-maxFandoms`).value);this.opts.maxRelations = this.getInt(document.getElementById(`${SN}-black-maxRelations`).value);this.opts.minIncomplete = this.getInt(document.getElementById(`${SN}-black-minIncomplete`).value);this.opts.minChapters = this.getInt(document.getElementById(`${SN}-black-minChapters`).value);this.opts.maxChapters = this.getInt(document.getElementById(`${SN}-black-maxChapters`).value, this.opts.minChapters);this.opts.minWords = this.getInt(document.getElementById(`${SN}-black-minWords`).value);this.opts.maxWords = this.getInt(document.getElementById(`${SN}-black-maxWords`).value, this.opts.minWords);this.opts.langs = document.getElementById(`${SN}-black-langs`).value;this.opts.show = document.getElementById(`${SN}-black-show`).checked;this.opts.pause = document.getElementById(`${SN}-black-pause`).checked;setStorage('blacklistTags', this.lists.Tag);setStorage('blacklistText', this.lists.Text);setStorage('blacklistAuthors', this.lists.Author);setStorage('blacklistOpts', this.opts);for (let el of document.querySelectorAll(`${this.blurb}[data-${SN}-visibility]`)) {el.removeAttribute(`data-${SN}-visibility`);el.removeAttribute(`data-${SN}-reasons`);}this.setVisibility();},html: function() {let blackMenu = document.createElement('li');blackMenu.id = `${SN}-black`;blackMenu.className = 'dropdown';blackMenu.innerHTML = `<a>Blacklist</a><ul class="menu dropdown-menu"><li><a class="${SN}-save" id="${SN}-black-save">SAVE</a></li><li class="${SN}-opts"><span>SHOW REASONS <input id="${SN}-black-show" type="checkbox" ${this.opts.show ? 'checked' : ''}></span><span>PAUSE <input id="${SN}-black-pause" type="checkbox" ${this.opts.pause ? 'checked' : ''}></span><span title="show only specified">languages<br><input type="text" id="${SN}-black-langs" spellcheck="false"title="separate languages by a space" value="${this.opts.langs}"></span></li><li class="${SN}-opts"><span title="for works in progress only">updated<br>max<input id="${SN}-black-minIncomplete" type="number" min="0" step="1"title="in months" value="${this.opts.minIncomplete}"></span><span>tags<br>max<input id="${SN}-black-maxTags" type="number" min="0" step="1" value="${this.opts.maxTags}"></span><span>fandoms<br>max<input id="${SN}-black-maxFandoms" type="number" min="0" step="1" value="${this.opts.maxFandoms}"></span><span>relations<br>max<input id="${SN}-black-maxRelations" type="number" min="0" step="1" value="${this.opts.maxRelations}"></span></li><li class="${SN}-opts"><span>chapters<br>min<input id="${SN}-black-minChapters" type="number" min="0" step="1"value="${this.opts.minChapters}">max <input id="${SN}-black-maxChapters" type="number" min="0" step="1"value="${this.opts.maxChapters}"></span><span>words<br>min<input id="${SN}-black-minWords" type="number" min="0" step="1"title="in thousands" value="${this.opts.minWords}">max <input id="${SN}-black-maxWords" type="number" min="0" step="1"title="in thousands" value="${this.opts.maxWords}"></span></li><li><span title="tags, fandoms, relations, characters, ratings, warnings, categories, status">tags</span><textarea id="${SN}-black-tags" spellcheck="false">${this.lists.Tag.join(',')}</textarea><span>titles, summaries</span><textarea id="${SN}-black-text" spellcheck="false">${this.lists.Text.join(',')}</textarea><span>authors</span><textarea id="${SN}-black-authors" spellcheck="false">${this.lists.Author.join(',')}</textarea></li><li class="${SN}-opts"><span title="comma">separator: ,</span><span title="match zero or more of any character (letter, white space, symbol...)">wildcard: *</span><span title="match two pair of words in any order">pair: &&</span><span title="hide relationships that include only one person of your favourite ship (only for tags)">only otp: &!</span></li></ul>`;document.querySelector('#header ul.primary.navigation.actions').appendChild(blackMenu);document.getElementById(`${SN}-black-save`).addEventListener('click', function() {Blacklist.setValues();this.textContent = 'SAVED';setTimeout(() => { this.textContent = 'SAVE'; }, 1000);});}};await Blacklist.getValues();Blacklist.setVisibility();Blacklist.html();} // END Feature.black AND Check.black()/** GLOBAL FUNCTIONS **/async function getStorage(key, def) {// def must be a stringreturn JSON.parse(await GM.getValue(key, def));}function setStorage(key, value) {// value can be any typeGM.setValue(key, value !== 'string' ? JSON.stringify(value) : value);}function addCSS(id, css) {// unique id because of styling user changesif (document.querySelector(`style#${id}`)) {document.querySelector(`style#${id}`).textContent = css;} else {let style = document.createElement('style');style.id = id;style.textContent = css;document.getElementsByTagName('head')[0].appendChild(style);}}function countTime(num) {// estimate reading timeif (!num) return '?';num = Math.round(Number(num) / Feature.wpm);let h = Math.floor(num / 60);let m = num % 60;return `${h > 0 ? `${h}hr ` : ''}${m > 0 ? `${m}min` : ''}` || '<1min';}function getScroll() {return Math.max(document.documentElement.scrollTop, window.scrollY, 0);}function setScroll(s) {window.scroll(0, s);}function getDocHeight() {return Math.max(document.documentElement.scrollHeight, document.documentElement.offsetHeight,document.body.scrollHeight, document.body.offsetHeight);}})();