// ==UserScript==
// @name w4tchdoge's AO3 Bookmark Maker
// @namespace https://github.com/w4tchdoge
// @version 1.0.3
// @description Modified/Forked from "Ellililunch AO3 Bookmark Maker" (https://greasyfork.org/en/scripts/458631). Script is out-of-the-box setup to automatically add title, author, status, summary, and last read date to the description in an "collapsible" section so as to not clutter the bookmark.
// @author w4tchdoge
// @homepage https://github.com/w4tchdoge/MISC-UserScripts
// @match *://archiveofourown.org/works/*
// @match *://archiveofourown.org/series/*
// @icon https://archiveofourown.org/favicon.ico
// @license GNU GPLv3
// @downloadURL none
// ==/UserScript==
/*
// THIS USERSCRIPT RELIES HEAVILY ON TEMPLATE LITERALS
// FOR INFORMATION ON WHETHER YOUR BROWSER IS COMPATIBLE WITH TEMPLATE LITERALS PLEASE VISIT THE FOLLOWING WEBPAGE
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#browser_compatibility
*/
// NOTICE TO NEW USERS
// Configuration area for the autogenerated portion of the bookmark at the bottom of the userscript
// Various settings that can be tweaked by the end user are near the top (please read the instructions and if you don't understand something, don't hesitate to PM me for clarification)
(function () {
/* constants that can be changed by the end user to affect how the script functions */
const divider = `\n\n`, /* string which is used to indicate where the bookmark should be split in half */
autoPrivate = false, /* if true, automatically checks the checkbox to private the bookmark */
bottomEntireWork = true, /* if true, checks if the current page is an entire work page or a work page that is not on the first chapter.
If the aforementioned checks are passed, clones the "Entire Work" button to the bottom nav bar to easily navigate to a page where the userscript will work
This is done due to the fact that the last read date will not update when updating a bookmark from the latest chapter */
simpleWorkSummary = false, /* if true, uses the original method to retrieve the work summary (least hassle, but includes the 'Summary' heading element which some users may find annoying);
if false, retrieves the work summary in a way that allows more flexibility when customising newBookmarkNotes */
falseSWS_asBlockquote = true, /* if not using the original work summary method, set whether you want to retrieve the summary as a blockquote */
/* for more information on the effects of changing simpleWorkSummary and falseSWS_asBlockquote, please look at where simpleWorkSummary is first used in the script, it should be around line 169 */
splitSelect = 1; /*
splitSelect changes which half of bookmarkNotes your initial bookmark is supposed to live in.
Valid values are 0 and 1.
e.g.
If you want the final bookmark (after pasting of autogenerated text) to look like the below text (and have configured it as such at the bottom of the script):
{bookmarkNotes}
{title} by {author}
{status}
{summary}
Then you can set divider = '
' and splitSelect = 0
What this does is it replaces anything AFTER the
with the autogenerated bookmark text you've defined at the bottom while keeping your own text (e.g. "@ Chapter 2" or "Chapter 8 ripped my heart out").
If you instead want something like the following:
{title} by {author}
{status}
{summary}
{bookmarkNotes}
Then you can set divider = '
' and splitSelect = 1, which replaces everything BEFORE your bookmark notes.
Another way to explain it is that the script works by taking the current contents of your bookmark and splitting it into two pieces along a user defined "divider", thus creating an array. Depending on the way you want the bookmark to be structured, the part of the bookmark that you want to keep could be before the divider or after the divider. splitSelect lets you tell the script which half of the array contains the bit of the bookmark you want to keep. If it's the first half splitSelect is 0, if it's the last half splitSelect is 1.
*/
// add main element that all querySelector operations will be done on
main = document.querySelector(`div#main`);
if (autoPrivate) { // for auto-privating your bookmarks
main.querySelector(`#bookmark_private`).checked = true;
}
// keeps any bookmark notes you've made previously
var bookmarkNotes = main.querySelector(`#bookmark_notes`).textContent.split(divider).at(`-${splitSelect}`);
// Define variables used in date configuration
var currdate = new Date(),
dd = String(currdate.getDate()).padStart(2, `0`),
mm = String(currdate.getMonth() + 1).padStart(2, `0`), //January is 0
yyyy = currdate.getFullYear(),
hh = String(currdate.getHours()).padStart(2, `0`),
mins = String(currdate.getMinutes()).padStart(2, `0`);
// Define variables used in bookmark configuration
var author,
words,
status,
title,
summary,
lastChapter,
latestChapterNumLength,
chapNumPadCount;
// Checks if the current page is either the first chapter of a work or the entire work
let currPgURL = window.location.href;
if (currPgURL.includes(`works`) && main.querySelector(`li.chapter.previous`) != null && bottomEntireWork) {
// If all above conditions are true, add a second "Entire Work" button at the bottom nav bar
// Clone the "Entire Work" button
var enti_work = main.querySelector(`li.chapter.entire`).cloneNode(true);
// Add padding to make it look more natural in the bottom nav bar
enti_work.style.paddingLeft = `0.5663em`;
// Get the "↑ Top" button that's in the bottom nav bar
let toTop_xp = `.//*[@id="feedback"]//*[@role="navigation"]//li[*[text()[contains(.,"Top")]]]`;
let toTop_btn = document.evaluate(toTop_xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
// Add the cloned "Entire Work" button after the "↑ Top" button in the bottom navbar
toTop_btn.after(enti_work);
}
// Look for HTML DOM element only present on series pages
var seriesTrue = document.evaluate(`.//*[@id="main"]//span[text()="Series"]`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
// Check if current page is a series page
if (seriesTrue != undefined) {
// Retrieve series information
// Retrieve series title
title = main.querySelector(`:scope > h2.heading`).textContent.trim();
// Retrieve series word count
words = document.evaluate(`.//*[@id="main"]//dl[contains(concat(" ",normalize-space(@class)," ")," stats ")]//dt[text()="Words:"]/following-sibling::*[1]/self::dd`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
// Retrieve series author
author = document.evaluate(`.//*[@id="main"]//dl[contains(concat(" ",normalize-space(@class)," ")," series ")][contains(concat(" ",normalize-space(@class)," ")," meta ")][contains(concat(" ",normalize-space(@class)," ")," group ")]//dt[text()="Creator:"]/following-sibling::*[1]/self::dd`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
// Retrieve series summary
summary = main.querySelector(`.series.meta.group .userstuff`).innerHTML;
// Retrieve series status
let pub_xp = `//dl[contains(concat(" ",normalize-space(@class)," ")," series ")][contains(concat(" ",normalize-space(@class)," ")," meta ")][contains(concat(" ",normalize-space(@class)," ")," group ")]//dl[contains(concat(" ",normalize-space(@class)," ")," stats ")]//dt[contains(text(), "Complete")]/following-sibling::*[1]`;
let complete = document.evaluate(pub_xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
var updated = main.querySelector(`.series.meta.group`).getElementsByTagName(`dd`)[2].textContent;
if (complete == `No`) {
status = `Updated: ${updated}`;
} else if (complete == `Yes`) {
status = `Completed: ${updated}`;
}
}
else {
// Retrieve work information
// Calculate appropriate padding count for lastChapter
latestChapterNumLength = document.evaluate(`.//*[@id="main"]//dl[contains(concat(" ",normalize-space(@class)," ")," stats ")]//dt[text()="Chapters:"]/following-sibling::*[1]/self::dd`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent.split(`/`).at(0).length;
if (latestChapterNumLength >= 3) {
chapNumPadCount = 3;
}
else {
chapNumPadCount = 2;
}
// Retrieve last chapter of work
lastChapter = `Chapter ${document.evaluate(`.//*[@id="main"]//dl[contains(concat(" ",normalize-space(@class)," ")," stats ")]//dt[text()="Chapters:"]/following-sibling::*[1]/self::dd`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent.split(`/`).at(0).padStart(chapNumPadCount, `0`)}`;
// Retrieve work title
title = main.querySelector(`#workskin .title.heading`).textContent.trim();
// Retrieve work work count
words = document.evaluate(`.//*[@id="main"]//dl[contains(concat(" ",normalize-space(@class)," ")," stats ")]//dt[text()="Words:"]/following-sibling::*[1]/self::dd`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
// Retrieve work author
author = main.querySelector(`#workskin > .preface .byline`).textContent.trim(); // fic author
// Retrieve work summary
if (simpleWorkSummary) { // the original methos to retrieve the work's summary
summary = document.getElementsByClassName(`summary`)[0].innerHTML;
// Example output of the above method:
// summary will be a var equal to the following string
// '\n Summary:
\n \n Lorem ipsum dolor...
\n
\n '
}
else if (!simpleWorkSummary && falseSWS_asBlockquote && main.querySelector(`.summary blockquote`) != null) { // new method #1
summary = main.querySelector(`.summary blockquote`).outerHTML;
// Example output of the above method:
// summary will be a var equal to the following string
// '\n Lorem ipsum dolor...
\n
'
}
else if (!simpleWorkSummary && !falseSWS_asBlockquote && main.querySelector(`.summary blockquote`) != null) { // new method #2
summary = main.querySelector(`.summary blockquote`).innerHTML.trim();
// Example output of the above method:
// summary will be a var equal to the following string
// 'Lorem ipsum dolor...
'
}
// Retrieve work status
if (document.getElementsByClassName(`status`).length != 0) {
// Retrieval method for multi-chapter works
status = `${main.querySelector(`dt.status`).textContent} ${main.querySelector(`dd.status`).textContent}`;
}
else {
// Retrieval method for single chapter works
status = `${main.querySelector(`dt.published`).textContent} ${main.querySelector(`dd.published`).textContent}`;
}
}
/* ///////////////// USER CONFIGURABLE SETTINGS ///////////////// */
/*
// Below are the configurations for the autogenerated bookmark content, including the date configuraton
// THE CONFIGURATION RELIES HEAVILY ON TEMPLATE LITERALS
// FOR MORE INFORMATION ON TEMPLATE LITERALS PLEASE VISIT THE FOLLOWING WEBPAGE
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
*/
/* ///////////////// Bookmark content configuration section ///////////////// */
/*
Variables that can be used when creating the string for newBookmarkNotes:
- date // Current date (and time) – User configurable in the Date configuration sub-section
- title // Title of the work or series
- author // Author of the work or series
- status // Status of the work or series. i.e. Completed: 2020-08-23, Updated: 2022-05-08, Published: 2015-06-29
- summary // Summary of the work or series
- words // Current word count of the work or series
Variables specific to series:
NONE
Variables specific to works:
- lastChapter // Last published chapter of the work or series
*/
/* //// Date configuration sub-section //// */
/*
// THE CONFIGURATION RELIES HEAVILY ON TEMPLATE LITERALS
// FOR MORE INFORMATION ON TEMPLATE LITERALS PLEASE VISIT THE FOLLOWING WEBPAGE
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
*/
// Setting the date
// Feel free to use your own date format, you don't need to stick to the presets here
var date = `${yyyy}/${mm}/${dd}`; // Date without time
// date = `${yyyy}/${mm}/${dd} ${hh}${mm}hrs`; // Date with time
console.log(`
w4tchdoge's AO3 Bookmark Maker UserScript – Log
--------------------
Date Generated: ${date}`
);
/* ///////////// Select from Presets ///////////// */
// To stop using a preset, wrap it in a block comment (add /* to the start and */ to the end); To start using a preset, do the opposite.
// ------------------------
// Preset 1 – With last read date
// To use this preset, scroll to the top where the constants are defined and set divider and splitSelect to the following values:
// divider = `\n\n`
// splitSelect = 1
var newBookmarkNotes = `Work Details
\t${title} by ${author}
\t${status}
\tWork Summary:
\t${summary}
(Approximate) Last Read: ${date}
${bookmarkNotes}`;
// ------------------------
// Preset 2 – Without last read date
// To use this preset, scroll to the top where the constants are defined and set divider and splitSelect to the following values:
// divider = `\n\n`
// splitSelect = 1
/* var newBookmarkNotes = `Work Details
\t${title} by ${author}
\t${status}
\tWork Summary:
\t${summary}
${bookmarkNotes}`; */
// ------------------------
// Preset 3 – Preset 1 but reversed
// To use this preset, scroll to the top where the constants are defined and set divider and splitSelect to the following values:
// divider = `
\n`
// splitSelect = 0
/* var newBookmarkNotes = `${bookmarkNotes}
Work Details
\t${title} by ${author}
\t${status}
\tWork Summary:
\t${summary}
(Approximate) Last Read: ${date} `; */
// ------------------------
// Preset 4 – Preset 2 but reversed
// To use this preset, scroll to the top where the constants are defined and set divider and splitSelect to the following values:
// divider = `
\n`
// splitSelect = 0
/* var newBookmarkNotes = `${bookmarkNotes}
Work Details
\t${title} by ${author}
\t${status}
\tWork Summary:
\t${summary}
(Approximate) Last Read: ${date} `; */
// ------------------------
// You are free to define your own string for the newBookmarkNotes variable as you see fit
// Fills the bookmark box with the autogenerated bookmark
document.getElementById("bookmark_notes").innerHTML = newBookmarkNotes;
})();