您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Change the font, size, width and background of a work + blacklist: hide works that contain certain tags or text, have too many tags/fandoms/relationships/chapters/words and other options + fullscreen reading mode + bookmarks: save the position where 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://codeberg.org/schegge // @description Change the font, size, width and background of a work + blacklist: hide works that contain certain tags or text, have too many tags/fandoms/relationships/chapters/words and other options + fullscreen reading mode + bookmarks: save the position where you stopped reading a fic + number of words for each chapter and estimated reading time // @version 3.6.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.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') !== 364) { setStorage('version', 364); 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):not(.skins)') && !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: min(calc(100% - 8em), 44em); z-index: 999; font-size: .9em; color: #000; background: #fff; padding: 1em; border: 1px solid #900;"> <b>AO3: Styling, Blacklist, Bookmarks</b> UPDATES (v3.6.4)<br><br> Bugs fixed:<br> - Adding "Anonymous" to the author field of the blacklist now works;<br> - Series links are now displayed correctly when in full screen mode. <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`, `/* Menu */ 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; } li[id|="${SN}"] .dropdown-menu input[type="checkbox"] { margin: 0; } li[id|="${SN}"] .dropdown-menu textarea { font-size: .9em; line-height: 1.3em; 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; } #${SN}-black .dropdown-menu { min-width: min(28em, 100vw); } #${SN}-black .dropdown-menu .${SN}-opts { text-align: center !important; } #${SN}-black .dropdown-menu .${SN}-opts > a, #${SN}-black .dropdown-menu .${SN}-optsFull > a { height: auto; font-size: .8em !important; text-transform: uppercase; padding: .3em 0 !important; margin: 0 !important; cursor: default; } #${SN}-black .dropdown-menu .${SN}-opts > a { width: 25%; flex: auto; } #${SN}-black .dropdown-menu input[type="text"] { width: 12em; } #${SN}-black-tags { height: 6em; } #${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 !important; bottom: 0; right: 0; margin: 0 1em 1em 0; padding: 0; text-align: right; font-size: .8rem; z-index: 999 !important; font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; } #${SN}-style:not(.${SN}-style-hide) { width: min(28em, calc(100% - 2em)); } #${SN}-style:not(.${SN}-style-hide) > ul { display: block !important; padding: .5em; margin: 0; border-radius: .3em; background-color: #ddd; color: #111; box-shadow: inset 0 0 5px #999; } #${SN}-style.${SN}-style-hide > ul { display: none; } #${SN}-style li { padding: .4em 0 .2em; margin: 0; border-bottom: 1px solid #888; } #${SN}-style input, #${SN}-style select { width: 60%; vertical-align: middle; } #${SN}-style button { margin: .3em .2em; } #${SN}-style-button { padding: 0 .3em; font-size: 1.2em !important; } .${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.setAttribute('aria-haspopup', 'true'); featureMenu.innerHTML = `<a class="dropdown-toggle" data-toggle="dropdown" data-target="#" 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.reload(); }); // 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.setAttribute('aria-haspopup', 'true'); bookMenu.innerHTML = '<a class="dropdown-toggle" data-toggle="dropdown" data-target="#">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`, `/* Fullscreen */ #outer.wrapper, div#outer.wrapper > * { display: none !important; } #workskin .preface { margin: 0; padding-bottom: 0; } .preface.group .module { padding-bottom: 0; text-align: center; } .preface.group .module h3.heading { display: inline; cursor: pointer; text-align: center; opacity: .5; font-style: italic; font-size: 100%; } .preface h3 + p { border: 3px solid rgba(0, 0, 0, .1); border-left: 0; border-right: 0; padding: .6em; margin: 0; } .preface.group .module .userstuff { background-color: #fff; } .preface.group .module > :not(h3), .preface.group .notes.module.hidden, .actions:not(div[class*="${SN}-book"]) li > a:not([href*="chapters"]):not([href="#workskin"]) { display: none; } .preface.group .module > h3:hover ~ .userstuff, .preface.group .module > .userstuff:hover, .afterword.preface.group .module > h3:hover + ul, .afterword.preface.group .module > ul:hover { display: block !important; position: absolute; width: 100%; max-height: 6em; transform: translateY(-100%); color: inherit; background-color: inherit; padding: 0.5em; text-align: left; margin: 0; overflow: auto; z-index: 999; cursor: pointer; } .actions:not(div[class*="${SN}-book"]) { margin-top: 2em; }` ); // .preface.group .module .userstuff { background-color: #fff; } -> in case Styling is disabled // to hide notes whith no author's words workskin.querySelectorAll(".preface.group .notes.module").forEach(note => { if (!note.querySelector('.userstuff')) note.classList.add('hidden'); }); 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); // change the url for prevoious and next chapter and top let bottomNav = document.querySelector('#feedback .actions'); bottomNav.querySelectorAll('li > a').forEach(a => { if (/chapter/i.test(a.textContent)) { a.href = a.href.replace('#workskin', '?bookmark'); } }); bottomNav.querySelector('a[href="#main"]').href = '#workskin'; // append the navigation to the workskin workskin.appendChild(bottomNav); }; 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`, `/* General Styling */ #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, div.preface .byline a, #workskin #chapters a, #chapters a:link, #chapters a:visited { color: inherit !important; } #workskin .actions { font-size: 1rem; font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif; } .chapter .preface { border-top: 0; margin-bottom: 0; margin-top: 0; } .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; font-size: .8em; } .preface.group .module { min-height: 0; } #workskin #chapters .preface .userstuff p, #workskin .preface .userstuff p, .userstuff details { margin: .1em auto; line-height: 1.1em; } div.preface .jump { margin-top: 1em; font-size: .9em; } .preface blockquote { padding: .6em; margin: 0; border-inline-width: 1px; border-inline-start-color: rgba(255, 255, 255, .2); } .afterword.preface .module h3.heading + ul { padding: .01em .6em; } .preface blockquote.userstuff, .afterword.preface .module h3.heading + ul { box-shadow: 0 0 2px rgba(0, 0, 0, .8), inset 0 0 2px rgba(255, 255, 255, .5); } .preface h3.title { background: repeating-linear-gradient(46deg, rgba(0, 0, 0, .05), rgba(0, 0, 0, .15) .49em, rgba(255, 255, 255, .2) .5em, rgba(255, 255, 255, .2) .51em); box-shadow: 0 0 2px rgba(0, 0, 0, .8), inset 0 0 2px rgba(255, 255, 255, .5); padding: .6em; margin: 0; } .preface h3.heading, .userstuff h3 { 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: 1px; 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' }, fonts: { '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'] }, inputs: function() { return { fontName: {label: 'Font', options: Object.keys(this.fonts), default: 'Default'}, colors: {label: 'Background', options: Object.keys(this.colors), default: 'light'}, textAlign: {label: 'Alignment', options: ['default', 'justify', 'left', 'center', 'right'], default: 'justify'}, fontSize: {label: 'Text Size', range: [80, 300], default: 100}, margins: {label: 'Page Margins', range: [5, 40], default: 7}, lineSpacing: {label: 'Line Spacing', range: [3, 14], default: 5} } }, getValues: async function() { Object.assign(this.opts, await getStorage('styling', '{}')); }, setValues: function() { setStorage('styling', this.opts); addCSS(`${SN}-userstyle`, `/* User Styling */ #workskin { font-family: ${this.fonts[this.opts.fontName]}; font-size: ${parseFloat((this.opts.fontSize/100).toFixed(2))}rem; 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, #workskin #chapters .userstuff p { line-height: ${parseFloat((this.opts.lineSpacing * 0.4).toFixed(2))}rem; ${this.opts.textAlign === 'default' ? '' :`text-align: ${this.opts.textAlign};`} } #workskin #chapters .userstuff p { margin: ${parseFloat((this.opts.lineSpacing * 0.5 - 1.4).toFixed(2))}rem auto; } #workskin .preface.group .heading { color: ${this.colors[this.opts.colors][1]} !important; } .preface.group .module { color: ${this.colors[this.opts.colors][1]}; background-color: ${this.colors[this.opts.colors][0]} !important; }` ); }, html: function() { this.setValues(); let inputs = this.inputs(); // the options displayed on the page let styleMenu = document.createElement('div'); styleMenu.id = `${SN}-style`; styleMenu.className = `${SN}-style-hide`; let h = ''; for (const [name, input] of Object.entries(inputs)) { h += `<li><label>${input.label}</label>`; if ('options' in input) { h += `<select id="${name}">`; for (const o of input.options) { h += `<option value="${o}" ${o === this.opts[name] ? 'selected' : ''}>${o}</option>`; } h += '</select>'; } else { h += `<input type="range" min="${input.range[0]}" max="${input.range[1]}" id="${name}" value="${this.opts[name]}">`; } h += '</li>'; } styleMenu.innerHTML = `<ul>${h}<button id="${SN}-style-save">save</button> <button id="${SN}-style-reset">reset</button></ul> <button id="${SN}-style-button">☰</button>`; document.body.appendChild(styleMenu); document.getElementById(`${SN}-style-save`).addEventListener('click', () => { let pos = getScroll() / getDocHeight(); for (const name in inputs) { this.opts[name] = styleMenu.querySelector(`#${name}`).value; } this.setValues(); setScroll(pos * getDocHeight()); }); document.getElementById(`${SN}-style-reset`).addEventListener('click', () => { let pos = getScroll() / getDocHeight(); styleMenu.parentElement.removeChild(styleMenu); for (const [name, input] of Object.entries(inputs)) { this.opts[name] = input.default; } this.html(); setScroll(pos * getDocHeight()); }); document.getElementById(`${SN}-style-button`).addEventListener('click', () => { styleMenu.classList.toggle(`${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() { for (let el of document.querySelectorAll(`${this.blurb}[data-${SN}-visibility]`)) { el.removeAttribute(`data-${SN}-visibility`); el.removeAttribute(`data-${SN}-reasons`); } if (this.opts.pause) return; for (let work of document.querySelectorAll(this.blurb)) { // fix for targeting also Anonymous works let h4 = work.querySelector('h4.heading'); if (/by[\s\n]+Anonymous$/.test(h4.textContent.trim())) { h4.innerHTML = h4.innerHTML.replace(/(-->\n\s+)Anonymous/m, '$1<a rel="author">Anonymous</a>'); } // check if blacklisted and why 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(',').map(s => s.trim()).filter(s => s.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() { // when changes are made manually on the menu 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); this.setVisibility(); }, updateValues: function() { // when new tags or authors are added by click document.getElementById(`${SN}-black-tags`).value = this.lists.Tag.join(', '); document.getElementById(`${SN}-black-authors`).value = this.lists.Author.join(', '); setStorage('blacklistTags', this.lists.Tag); setStorage('blacklistAuthors', this.lists.Author); this.setVisibility(); }, html: function() { let blackMenu = document.createElement('li'); blackMenu.id = `${SN}-black`; blackMenu.className = 'dropdown'; blackMenu.setAttribute('aria-haspopup', 'true'); blackMenu.innerHTML = `<a class="dropdown-toggle" data-toggle="dropdown" data-target="#">Blacklist</a> <ul class="menu dropdown-menu"> <li> <a class="${SN}-save" id="${SN}-black-save">SAVE</a> </li><li class="${SN}-opts"> <a>SHOW REASONS <input id="${SN}-black-show" type="checkbox" ${ this.opts.show ? 'checked' : ''}></a> <a>PAUSE <input id="${SN}-black-pause" type="checkbox" ${ this.opts.pause ? 'checked' : ''}></a> </li><li class="${SN}-opts"> <a 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}"></a> <a>tags<br>max <input id="${SN}-black-maxTags" type="number" min="0" step="1" value="${ this.opts.maxTags}"></a> <a>relations<br>max <input id="${SN}-black-maxRelations" type="number" min="0" step="1" value="${ this.opts.maxRelations}"></a> </li><li class="${SN}-opts"> <a>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}"></a> <a>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}"></a> </li><li class="${SN}-opts"> <a>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}"></a> <a 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}"></a> </li><li class="${SN}-optsFull"> <a title="tags, fandoms, relations, characters, ratings, warnings, categories, status">tags</a> <textarea id="${SN}-black-tags" spellcheck="false">${ this.lists.Tag.join(', ')}</textarea> <a>titles, summaries</a> <textarea id="${SN}-black-text" spellcheck="false">${ this.lists.Text.join(', ')}</textarea> <a>authors</a> <textarea id="${SN}-black-authors" spellcheck="false">${ this.lists.Author.join(', ')}</textarea> </li><li class="${SN}-opts"> <a title="comma">separator: ,</a> <a title="match zero or more of any character (letter, white space, symbol...)">wildcard: *</a> <a title="match two pair of words in any order">pair: &&</a> <a title="hide relationships that include only one person of your favourite ship (only for tags)">only otp: &!</a> </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); }); }, altClick: function(event) { if (event.altKey) { if (event.target.classList.contains('tag') && !this.lists.Tag.includes(event.target.textContent)) { event.preventDefault(); this.lists.Tag.push(event.target.textContent); this.updateValues(); } else if (event.target.getAttribute('rel') === 'author' && !this.lists.Author.includes(event.target.textContent)) { event.preventDefault(); this.lists.Author.push(event.target.textContent); this.updateValues(); } } } }; await Blacklist.getValues(); Blacklist.setVisibility(); Blacklist.html(); document.querySelector('.index.group').addEventListener("click", Blacklist.altClick.bind(Blacklist)); } // 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); } })();