- // ==UserScript==
- // @name AO3: Fic's Style, Blacklist, Bookmarks
- // @namespace https://codeberg.org/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
- // @version 3.6.2
- // @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.js
- if (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 page
- const Check = {
- // script version
- version: async function() {
- if (await getStorage('version', '1') !== 362) {
- setStorage('version', 362);
- return true;
- }
- return false;
- },
- // on search pages but not on personal user profile
- black: 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)navigate
- work: function() {
- return /\/works\/\d+(\/chapters\/\d+)?(?!.*navigate)/.test(window.location.pathname);
- },
- // Full Screen
- fullScreen: false
- };
-
- // new version check
- if (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.2)<br><br> -- in the blacklist, added option "min fandoms": it hides works that has less that amount of fandoms listed
- <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' menu
- addCSS(`${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: 12em; }
-
- #${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 found
- if (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 chapter
- if (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 exists
- if (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 position
- if (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 exist
- found = true;
- }
- break;
- }
- }
- return found;
- },
- cancel: function(url) {
- let found = this.checkIfExist('cancel', url);
- // !== false because it can return 0 for the index
- if (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();
-
- // Fullscreen
- if (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 screen
- let 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 user
- const 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 color
- light: ['#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 page
- let 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 chapters
- if (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 spaces
- document.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,
- maxRelations: 0,
- minIncomplete: 0,
- minFandoms: 0,
- maxFandoms: 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;
- },
- 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());
- },
- getFandoms: function(work) {
- if ((this.opts.minFandoms || this.opts.maxFandoms) && work.querySelector('.header .fandoms .tag')) {
- let numFandoms = work.querySelectorAll('.header .fandoms .tag').length;
- if (this.opts.minFandoms && numFandoms < this.opts.minFandoms ||
- this.opts.maxFandoms && numFandoms > this.opts.maxFandoms) {
- return `Fandoms: ${numFandoms}`;
- }
- }
- return [];
- },
- 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*1000
- let 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;
-
- // wildcard
- pattern = pattern.replace(/\*/g, '.*');
- // match 2 words in any order
- pattern = pattern.replace(/(.+)&&(.+)/, '(?=.*$1)(?=.*$2).*');
- // only otp
- if (elem.parent === 'relationships') {
- // to delete fandom's name in the tag
- elem.text = elem.text.replace(/\(.+\)$/, '');
-
- pattern = pattern.replace(/(.+)&!(.+)/, '(?=.*\\/)((?=.*$1)(?!.*$2)|(?=.*$2)(?!.*$1)).*');
- }
-
- let regex;
- if (flag === 'free') regex = new RegExp(pattern, 'i'); // for text
- else 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 tag
- break;
- }
- }
- 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.getFandoms(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.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.maxRelations = this.getInt(document.getElementById(`${SN}-black-maxRelations`).value);
- this.opts.minIncomplete = this.getInt(document.getElementById(`${SN}-black-minIncomplete`).value);
- this.opts.minFandoms = this.getInt(document.getElementById(`${SN}-black-minFandoms`).value);
- this.opts.maxFandoms = this.getInt(document.getElementById(`${SN}-black-maxFandoms`).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>
- </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>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 class="${SN}-opts">
- <span>fandoms<br>min
- <input id="${SN}-black-minFandoms" type="number" min="0" step="1"
- value="${this.opts.minFandoms}">
- max <input id="${SN}-black-maxFandoms" type="number" min="0" step="1"
- value="${this.opts.maxFandoms}"></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>
- <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 string
- return JSON.parse(await GM.getValue(key, def));
- }
- function setStorage(key, value) {
- // value can be any type
- GM.setValue(key, value !== 'string' ? JSON.stringify(value) : value);
- }
-
- function addCSS(id, css) {
- // unique id because of styling user changes
- if (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 time
- if (!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);
- }
-
- })();