// ==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
// @author Schegge
// @match *://archiveofourown.org/*
// @match *://*.archiveofourown.org/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM.setValue
// @downloadURL none
// ==/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') !== 36) {
setStorage('version', 36);
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()) {
// retrieve saved value from localstorage if they exist
if (localStorage.getItem(`ficstyle_feature`)) await GM.setValue('feature', localStorage.getItem(`ficstyle_feature`))
if (localStorage.getItem(`ficstyle_styling`)) await GM.setValue('styling', localStorage.getItem(`ficstyle_styling`))
if (localStorage.getItem(`ficstyle_bookmarks`)) await GM.setValue('bookmarks', localStorage.getItem(`ficstyle_bookmarks`))
if (localStorage.getItem(`ficstyle_blacklist`)) await GM.setValue('blacklistTags', localStorage.getItem(`ficstyle_blacklist`))
if (localStorage.getItem(`ficstyle_blacklist_opts`)) await GM.setValue('blacklistOpts', localStorage.getItem(`ficstyle_blacklist_opts`))
// notification
document.body.insertAdjacentHTML('beforeend', `
You can now blacklist works that have too many tags, have a number of chapters below what you set for it, and contain certain words or phrases in the title and in the summary. I've changed how to blacklist authors, if you were hiding someone, you have to move them in the new designated area without the @.
I've moved your saved values from the browser local storage to the userscript manager storage, there shouldn't be any loss on your part, if otherwise please contact me on greasyfork, they aren't lost.
`;
document.querySelector('#header > ul').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, 10), 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) {
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: [],
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 book = false;
this.list.some((bm, i) => {
// check if the same fic already exists
if (bm[0].split('/chapters/')[0] === url.split('/chapters/')[0]) {
// i need the index to delete the old bookmark (for change or delete)
if (what === 'cancel') {
book = i;
return true;
// check if the same chapter
} else if (bm[0] === url) {
// retrieve the bookmark position
if (what === 'book') {
book = bm[2];
// if the bookmark is in %
if (book.toString().includes('%')) book = parseFloat(book.replace('%', '')) * getDocHeight();
// just check if a bookmark exist
} else {
book = true;
}
return true;
}
}
return false;
});
return book;
},
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 = '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.setValues();
this.style.display = 'none';
this.previousSibling.style.opacity = '.4';
};
this.list.forEach(item => {
let bookMenuLi = document.createElement('li');
bookMenuLi.className = `${SN}-opts`;
bookMenuLi.innerHTML = `${item[1]}`;
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 = '