// ==UserScript==
// @name AO3: Fic's Style, Blacklist, Bookmarks
// @namespace https://github.com/Schegge
// @version 3.5.3
// @description Change font, size, width, background... of a work + blacklist: hide works that contain certains tags, have too many 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
// @author Schegge
// @include http*://archiveofourown.org/*
// @grant none
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjc0REE4QjE3Qjk1ODExRTY4MEZCQTc3RERGMTNGQ0Y0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjc0REE4QjE4Qjk1ODExRTY4MEZCQTc3RERGMTNGQ0Y0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzREQThCMTVCOTU4MTFFNjgwRkJBNzdEREYxM0ZDRjQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzREQThCMTZCOTU4MTFFNjgwRkJBNzdEREYxM0ZDRjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz78uZUsAAACeUlEQVR42uxZvW7bMBCWDaFLuih9A3bJrqJLVhVdigwFZGQPIHdpO9aPYC+dGz1CNXXpEiFzFq0NMtjIE0QvYNQ9Bh+BA6Mf0rKkBuABHyxQJPHxdPfdSZ7sdjvvOdrUe6bmiDvijrgj7og/mn/oDX/4T7YMCAkhxLW0nJARNnzip+32v/H4N8KaEIHsipASBMYv2WHG9TizS3haEp1r96S3C8yRT+IdobTZfNLUq1Q89iYLQEJ6Nib8RCi8NjicfBKLsUJFhsUViCwxlrasSdlaMZaqJPi9YCSKljX8fjwG8YQl2Vs2nhusVXPCMYjHB9gjGJq4gNyV3oA2PVCYyFhd1BzK5OAm+dAL8RTarHs9NAiPUYirpFSkM+3+R8Pc2FSs7ZV4BG+XNbp93uD1gOn9fMgYF/BYpukyb5z+oihF2tozwh/CS8KtasBsKnUX4jFI5jXVUO3/G+V/jUPcE37hkCeEL6zqBkMQT2pKuj72gnBMmKEneYXQ+sAOnsLrcd/EY4RKFfFSG1dzC5C80/twliPGHvc7EC8aEm9TMX+F6zcNWp6bEtinrVUvATbW1N5GiG+pLKlpa+t3SMo2CQuZ3An2FsTbYHV/ZePtfT2+RgyvDKrig5a08wY9l8k+I49nfSSnSjSTzfUkTWqSr2ROWPalKklFkTHptZV9RYgENcks+lAVFacLizXqE4Qi9Bma7mmhJmrUqLPHA1S/fYyH1bGm2zxRPRun+AbKECFEBItDVVCKmnZUYG3I3kW5fUfneEM4xfyZTYfYRnxZ0SDxF+Mc30Sqkrgp0Y4I7wnXUJqN7WOcuP+AHHFH3BF3xB1xR9zC/gkwALX7mp/233xAAAAAAElFTkSuQmCC
// @downloadURL none
// ==/UserScript==
(function() {
const NAMESPACE = 'ficstyle';
/** FEATURES **/
const Feature = {
style: true,
book: true,
black: true,
wpm: 250
}
// Object.assign() changes only the same keys
Object.assign(Feature, getStorage(`${NAMESPACE}_feature`, '{}'));
// check which page
const Check = {
// script version
version: function() {
if (getStorage(`${NAMESPACE}_version`, '1') !== 353) {
setStorage(`${NAMESPACE}_version`, 353);
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 notification
if (Check.version()) {
document.body.insertAdjacentHTML('beforeend', `
You can now blacklist authors by putting @author_name in the text area of the blacklist (wildcards are allowed, so @*something* is ok and it'll only target the authors' names).
`;
document.querySelector('#header > ul').appendChild(featureMenu);
document.getElementById(`${NAMESPACE}-feature-save`).addEventListener('click', function() {
Feature.style = document.getElementById(`${NAMESPACE}-feature-style`).checked;
Feature.book = document.getElementById(`${NAMESPACE}-feature-book`).checked;
Feature.black = document.getElementById(`${NAMESPACE}-feature-black`).checked;
let wpm = document.getElementById(`${NAMESPACE}-feature-wpm`).value;
Feature.wpm = wpm ? Math.min(Math.max(parseInt(wpm, 10), 0), 1000) : 0;
setStorage(`${NAMESPACE}_feature`, Feature);
this.textContent = 'SAVING...';
window.location.replace(window.location.href);
});
// add estimated reading time for every fic found
if (Feature.wpm) {
document.querySelectorAll('dl.stats dd.words').forEach(w => {
let numWords = w.textContent.replace(/,/g, '');
w.insertAdjacentHTML('afterend', `
Time:
${countTime(numWords)}
`);
});
}
/** BOOKMARKS **/
let Bookmarks = {};
if (Feature.book) {
Bookmarks = {
list: [],
get: function() {
this.list = getStorage(`${NAMESPACE}_bookmarks`, '[]');
},
set: function() {
setStorage(`${NAMESPACE}_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;
for (let i = 0, len = this.list.length; i < len; i++) {
// check if the same fic already exists
if (this.list[i][0].split('/chapters/')[0] === url.split('/chapters/')[0]) {
// i need the index to delete the old bookmark (for change or cancel)
if (what === 'cancel') {
return i;
// check if the same chapter
} else if (this.list[i][0] === url) {
// retrieve the bookmark position
if (what === 'book') {
let book = this.list[i][2];
// if the bookmark is in %
if (book.toString().includes('%')) {
book = parseFloat(book.replace('%', ''));
book *= getDocHeight();
}
return book;
}
// just check if a bookmark exist
return true;
}
}
}
return false;
},
cancel: function(url) {
let found = this.checkIfExist('cancel', url);
if (found !== false) this.list.splice(found, 1);
},
getNew: function() {
this.cancel();
this.list.push([this.getUrl, this.getTitle(), this.getPosition()]);
this.set();
},
html: function() {
let bookMenu = document.createElement('li');
bookMenu.id = `${NAMESPACE}-book`;
bookMenu.className = 'dropdown';
bookMenu.innerHTML = 'Bookmarks';
let bookMenuDrop = document.createElement('ul');
bookMenuDrop.className = 'menu dropdown-menu';
bookMenu.appendChild(bookMenuDrop);
document.querySelector('#header > ul').appendChild(bookMenu);
if (this.list.length) {
let self = this;
let clickDelete = function() {
self.cancel(this.getAttribute('data-url'));
self.set();
this.style.display = 'none';
this.previousSibling.style.opacity = '.4';
};
this.list.forEach(item => {
let bookMenuLi = document.createElement('li');
bookMenuLi.innerHTML = `${item[1]}`;
let bookMenuDelete = document.createElement('a');
bookMenuDelete.className = `${NAMESPACE}-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 = '