// ==UserScript==
// @name [AO3] Kat's Tweaks: Bookmarking
// @author Katstrel
// @description Bookmark tracking, tagging, and more.
// @version 1.0.2
// @history 1.0.2 - fixed list page series blurbs from not displaying bookmark style
// @history 1.0.1 - disabled comment tag feature and buttons at the top of series pages
// @namespace https://github.com/Katstrel/Kats-Tweaks-and-Skins
// @include https://archiveofourown.org/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=archiveofourown.org
// @grant none
// ==/UserScript==
"use strict";
let DEBUG = false;
// তততততততত SETTINGS তততততততত //
let SETTINGS = {
bookmarking: {
enabled: true,
dateFormat: "Month/Year",
defaultNote: "No Notes",
details: "Tracking",
includeFandom: false,
newBookmarksPrivate: true,
newBookmarksRec: false,
hideDefaultToreadBtn: true,
showUpdatedBookmarks: true,
databaseInfo: [
{
keyID: "Bookmarked",
tagLabel: "Bookmarked",
enabled: true,
},
{
keyID: "Checked",
tagLabel: "Checked",
enabled: true,
},
{
keyID: "Commented",
tagLabel: "Commented",
enabled: false,
},
{
keyID: "Kudosed",
tagLabel: "Kudosed",
enabled: false,
},
{
keyID: "Series",
tagLabel: "Series",
enabled: true,
},
{
keyID: "Subscribed",
tagLabel: "Subscribed",
enabled: false,
},
],
databaseTags: [
{
keyID: "toread",
tagLabel: "To Read",
posLabel: "📚 Mark as To Read",
negLabel: "🧹 Remove from To Read",
btnHeader: true,
btnFooter: false,
},
{
keyID: "awaitupdate",
tagLabel: "Awaiting Update",
posLabel: "📖 Add to Awaiting Update",
negLabel: "📕 Remove from Awaiting Update",
btnHeader: false,
btnFooter: true,
},
{
keyID: "finished",
tagLabel: "Finished Reading",
posLabel: "✔️ Mark as Finished",
negLabel: "🗑️ Remove from Finished",
btnHeader: false,
btnFooter: true,
},
{
keyID: "favorite",
tagLabel: "Favorite",
posLabel: "❤️ Add to Favorites",
negLabel: "💔 Remove from Favorites",
btnHeader: true,
btnFooter: true,
},
],
databaseWord: [
{
keyID: "short",
tagLabel: "Short Story | Under 10k",
wordMin: 0,
wordMax: 10000,
},
{
keyID: "novella",
tagLabel: "Novella | 10k to 50k",
wordMin: 10000,
wordMax: 50000,
},
{
keyID: "novel",
tagLabel: "Novel | 50k to 100k",
wordMin: 50000,
wordMax: 100000,
},
{
keyID: "longfic",
tagLabel: "Longfic | Over 100k",
wordMin: 100000,
wordMax: Infinity,
},
],
}
};
// তততততত STOP SETTINGS তততততত //
class Bookmarking {
constructor(settings, moduleID) {
this.id = moduleID;
this.settings = settings.bookmarking;
this.request = new RequestManager();
this.storage = new StorageManager();
this.username = localStorage.getItem("KT-SavedUsername");
this.divider = "\nতততততততততত";
this.descrip = "\n⟡˖*°࿐ ✦ Summary:";
this.storage.init(`${this.id}-INFO-Bookmarked`);
this.storage.init(`${this.id}-INFO-Checked`);
}
getBookmarkData() {
return {
workId: this.workID,
id: this.bookmarkID,
pseudId: this.pseudID,
items: this.dataItems,
notes: this.notes,
tags: this.tags,
collections: this.collections,
isPrivate: this.private,
isRec: this.rec
};
}
getDataItem(dataItems, index) {
let value = Array.from(dataItems[index].querySelectorAll('li.added.tag')).map(element => {
return element.textContent.slice(0, -2).trim();
});
return value;
}
getWordCount(blurb) {
let words = blurb.querySelector('dd.words').innerText;
if (words.includes(",")) {
words = words.replaceAll(",", "");
}
if (words.includes(" ")) {
words = words.replaceAll(" ", "");
}
if (words.includes(" ")) {
words = words.replaceAll(/\s/g, "");
}
let wordsINT = parseInt(words);
DEBUG && console.log(`[Kat's Tweaks] Work Word Count: ${wordsINT}`);
return wordsINT;
}
makeNotes() {
let note = `${this.userNotes}`;
note += `${this.divider}`;
if (!this.isSeries) {
note += `\nLast Read: ${this.getTime()} \(${this.chapter}\)`;
}
// Tracking Block
note += `\n\<details\>\<summary\>${this.settings.details}\</summary\>`;
if (!this.isSeries && this.settings.includeFandom) {
note += `\nFandom: ${unzipArray(this.fandom)}`;
}
note += `\nAuthor: ${unzipArray(this.author)}`;
note += `\nTitle: \<a href="https://archiveofourown.org/${this.isSeries ? 'series' : 'works'}/${this.workID}"\>${this.title}\</a\>`;
if (!this.isSeries) {
note += `\nSeries: ${unzipArray(this.series)}`;
}
// Summary Block
note += `${this.descrip}\<blockquote\>${this.summary}\</blockquote\>\</details\>`;
return note
function unzipArray(array) {
let x = "";
for (let i = 0; i < array.length; i++) {
x += `\<a href="${array[i].href}"\>${array[i].innerText}\</a\> `;
}
return x;
}
}
updateStorage(blurb, storageID, tags) {
// Info Tags
this.settings.databaseInfo.slice(2).forEach(({keyID, tagLabel}) => {
this.storage.init(`${this.id}-INFO-${keyID}`);
const isTagged = tags.includes(tagLabel);
if (isTagged) {
this.storage.addIdToCategory(`${this.id}-INFO-${keyID}`, storageID);
blurb.classList.add(`${this.id}-INFO-${keyID}`);
}
else {
this.storage.removeIdFromCategory(`${this.id}-INFO-${keyID}`, storageID);
}
});
// Database Tags
this.settings.databaseTags.forEach(({keyID, tagLabel}) => {
this.storage.init(`${this.id}-TAGS-${keyID}`);
const isTagged = tags.includes(tagLabel);
if (isTagged) {
this.storage.addIdToCategory(`${this.id}-TAGS-${keyID}`, storageID);
blurb.classList.add(`${this.id}-TAGS-${keyID}`);
}
else {
this.storage.removeIdFromCategory(`${this.id}-TAGS-${keyID}`, storageID);
}
});
}
statusTags(blurb, wordCount) {
const tagComplete = "Complete";
if (!this.isSeries) {
if (document.getElementsByClassName("status").length != 0) {
// for multichapters
if (document.getElementsByClassName("status")[0].innerHTML == "Completed:") {
console.log(`[Kat's Tweaks] Adding tag: ${tagComplete}`);
blurb.querySelector('#bookmark_tag_string_autocomplete').value = `${tagComplete}, `;
this.tags.push(tagComplete);
}
}
else {
// for single chapter fics
console.log(`[Kat's Tweaks] Adding tag: ${tagComplete}`);
blurb.querySelector('#bookmark_tag_string_autocomplete').value += `${tagComplete}, `;
this.tags.push(tagComplete);
}
}
else {
console.log(`[Kat's Tweaks] Adding tag: Series`);
blurb.querySelector('#bookmark_tag_string_autocomplete').value += `${'Series'}, `;
this.tags.push('Series');
if (blurb.querySelector('dl.stats').innerHTML.includes('Yes')) {
console.log(`[Kat's Tweaks] Adding tag: ${tagComplete}`);
blurb.querySelector('#bookmark_tag_string_autocomplete').value += `${tagComplete}, `;
this.tags.push(tagComplete);
}
}
this.settings.databaseWord.forEach(({tagLabel, wordMin, wordMax}) => {
if (wordMin <= wordCount && wordCount < wordMax) {
console.log(`[Kat's Tweaks] Adding tag: ${tagLabel}`);
blurb.querySelector('#bookmark_tag_string_autocomplete').value += `${tagLabel}, `;
this.tags.push(tagLabel);
}
});
}
formNoteButtons(scope, query, workID, descrip, summary, notes, isBlurb) {
if (this.workID == this.bookmarkID) { return; }
const summaryWork = this.isSeries ? document.querySelector("dl.series.meta.group blockquote.userstuff") : document.querySelector("div.preface.group div.summary.module blockquote.userstuff");
const summaryChap = this.isSeries ? "No Chapter Summary" : document.querySelector("div.chapter.preface.group div.summary.module blockquote.userstuff");
let newNotes = notes;
let id = this.id;
// Update the chapter on works
if (!isBlurb && !this.isSeries) {
scope.querySelector('#notes-field-description').after(Object.assign(document.createElement('input'), {
type: 'button',
id: `${id}-${workID}-savechapter`,
class: `${id}-formButton`,
value: `🔖 Update Last Read`,
}));
// Add Click Listeners
let genNote = this.makeNotes();
scope.querySelectorAll(`#${id}-${workID}-savechapter`).forEach(button => {
button.addEventListener('click', (event) => {
event.preventDefault();
newNotes = genNote;
DEBUG && console.log(`[Kat's Tweaks] Updating Last Read.`, newNotes);
scope.querySelector(query).innerHTML = newNotes;
button.value = `🎉 Last Read Updated!`;
});
});
}
// Update summary if available
if ((summaryWork && !(summaryWork == summaryChap)) || isBlurb) {
scope.querySelector('#notes-field-description').after(Object.assign(document.createElement('input'), {
type: 'button',
id: `${id}-${workID}-summary`,
class: `${id}-formButton`,
value: `🖋️ Update Summary`,
}));
// Add Click Listeners
scope.querySelectorAll(`#${this.id}-${workID}-summary`).forEach(button => {
button.addEventListener('click', (event) => {
event.preventDefault();
newNotes = `${newNotes.split(descrip)[0]}${descrip}<blockquote>${summary}</blockquote></details>`;
DEBUG && console.log(`[Kat's Tweaks] Updating Summary.`, newNotes);
scope.querySelector(query).innerHTML = newNotes;
button.value = `🎉 Summary Updated!`;
});
});
}
// Remove buttons when textbox is highlighted due to mirror
[ "change", "keydown", "keyup", "mousedown", "mouseup" ].forEach(function(event) {
scope.querySelector(query).addEventListener(event, function(e) {
if (document.getElementById(`${id}-${workID}-summary`)) {
document.getElementById(`${id}-${workID}-summary`).remove();
}
if (document.getElementById(`${id}-${workID}-savechapter`)) {
document.getElementById(`${id}-${workID}-savechapter`).remove();
}
});
});
}
formTagButtons(blurb, workID) {
let form = blurb.querySelector('#bookmark-form');
form.querySelector('#tag-string-description').after(Object.assign(document.createElement('div'), {
id: `${this.id}-tags-${workID}`,
className: `${this.id}-tagContainer`,
}));
let tags = this.getDataItem((form.querySelectorAll('#bookmark-form form dd')), 1);
let tagInputBox = form.querySelector('.input #bookmark_tag_string_autocomplete');
let tagContainer = document.getElementById(`${this.id}-tags-${workID}`);
tagContainer.append(Object.assign(document.createElement('br')));
// Create the buttons
this.settings.databaseTags.forEach(({keyID, tagLabel, posLabel, negLabel}) => {
const isTagged = tags.includes(tagLabel);
let button = Object.assign(document.createElement('input'), {
type: 'button',
id: `${this.id}-${workID}-${keyID}-btnForm`,
class: `${this.id}-formButton`,
value: `${isTagged ? negLabel : posLabel}`,
});
if (button.value == posLabel) {
tagContainer.append(button);
}
});
// Add event listeners
this.settings.databaseTags.forEach(({keyID, tagLabel, posLabel, negLabel}) => {
const buttons = document.querySelectorAll(`#${this.id}-${workID}-${keyID}-btnForm`);
buttons.forEach((button) => {
button.addEventListener('click', (event) => {
event.preventDefault();
const isTagged = tags.includes(tagLabel);
if (isTagged) {
DEBUG && console.log(`[Kat's Tweaks] Removing ${tagLabel} from the input box.`);
if (tagInputBox.value) {
tagInputBox.value = `${tagInputBox.value.split(`${tagLabel}, `)[0]}${tagInputBox.value.split(`${tagLabel}, `)[1]}`;
}
tags.splice(tags.indexOf(tagLabel), 1);
}
else {
DEBUG && console.log(`[Kat's Tweaks] Adding ${tagLabel} to the input box.`);
tagInputBox.value += `${tagLabel}, `;
tags.push(tagLabel);
}
buttons.forEach((btn) => {
btn.value = isTagged ? posLabel : negLabel;
});
});
});
});
}
async requestHandler(bookmarkData, workID, forceBookmarkPage) {
const authenticityToken = this.getAuthenticityToken();
if (workID !== this.bookmarkID) {
await this.request.updateBookmark(this.bookmarkID, authenticityToken, bookmarkData);
if (forceBookmarkPage) {
window.location.href = `https://archiveofourown.org/bookmarks/${this.bookmarkID}`;
}
DEBUG && console.log(`[Kat's Tweaks] Updated bookmark ID: ${this.bookmarkID}`);
}
else {
bookmarkData.isPrivate = this.settings.newBookmarksPrivate;
bookmarkData.isRec = this.settings.newBookmarksRec;
this.bookmarkID = await this.request.createBookmark(workID, authenticityToken, bookmarkData);
window.location.href = `https://archiveofourown.org/bookmarks/${this.bookmarkID}`;
DEBUG && console.log(`[Kat's Tweaks] Created bookmark ID: ${this.bookmarkID}`);
}
}
// Retrieve the authenticity token from a meta tag
getAuthenticityToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : null;
}
}
class BookPage extends Bookmarking {
constructor(settings, moduleID) {
super(settings, moduleID);
this.bmForm = document.getElementById('bookmark-form');
this.bmNotes = document.getElementById('bookmark_notes');
this.bmTags = document.getElementById('bookmark_tag_string_autocomplete');
this.blurb = document.querySelector('dl.meta.group');
if (document.querySelector('dl.series.meta.group')) { this.isSeries = true; }
DEBUG && console.log(`[Kat's Tweaks] Bookmark form found. Is series: ${this.isSeries}`);
this.workID = document.URL.split('/')[4].split('#')[0].split('?')[0];
this.storageID = this.isSeries ? `${this.workID}S` : this.workID;
this.userNotes = this.getUserNotes();
this.timestamp = this.getTime();
this.chapter = this.getChapter();
this.fandom = this.isSeries ? "No Fandom" : document.querySelectorAll("dd.fandom.tags li a");
this.author = this.isSeries ? document.querySelectorAll('dl.series.meta.group dd a[rel="author"]') : document.querySelectorAll("div.preface.group h3.byline.heading a");
this.title = this.isSeries ? document.querySelector("h2.heading").innerText : document.querySelector("div.preface.group h2.title.heading").innerText;
this.series = document.querySelectorAll("dl.work.meta.group span.series span.position a");
this.summary = this.getSummary();
this.words = this.getWordCount(this.blurb);
this.bookmarkID = document.querySelector('div#bookmark_form_placement form') ? document.querySelector('div#bookmark_form_placement form').getAttribute('action').split('/')[2] : null;
this.pseudID = this.getPseudID();
this.dataItems = document.querySelectorAll('#bookmark-form form dd');
this.notes = this.bmNotes.innerText;
this.tags = this.getDataItem(this.dataItems, 1);
this.collections = this.getDataItem(this.dataItems, 2);
this.private = document.querySelector('#bookmark_private').checked;
this.rec = document.querySelector('#bookmark_rec').checked;;
if (this.workID == this.bookmarkID) {
DEBUG && console.log(`[Kat's Tweaks] Not bookmarked! WorkID: ${this.workID} | BookmarkID: ${this.bookmarkID}`);
this.private = this.settings.newBookmarksPrivate;
this.rec = this.settings.newBookmarksRec;
document.getElementById('bookmark_private').checked = this.private;
document.getElementById('bookmark_rec').checked = this.rec;
this.storage.removeIdFromCategory(`${this.id}-INFO-Bookmarked`, this.storageID);
}
else {
DEBUG && console.log(`[Kat's Tweaks] BookmarkID found for ${this.storageID}`);
this.storage.addIdToCategory(`${this.id}-INFO-Bookmarked`, this.storageID);
this.blurb.classList.add(`${this.id}-INFO-Bookmarked`);
}
DEBUG && console.log(`[Kat's Tweaks] Initialized Bookmarking Page with bookmark data:`);
DEBUG && console.table({
workId: this.workID,
id: this.bookmarkID,
pseudId: this.pseudID,
items: this.dataItems,
notes: this.notes,
tags: this.tags,
collections: this.collections,
isPrivate: this.private,
isRec: this.rec,
userNotes: this.userNotes,
time: this.timestamp,
chap: this.chapter,
fandom: this.fandom,
author: this.author,
title: this.title,
series: this.series,
summary: this.summary,
words: this.words,
makeNotes: this.makeNotes(),
});
if (document.querySelector('ul.work.navigation.actions li.mark') && this.settings.hideDefaultToreadBtn) {
document.querySelector('ul.work.navigation.actions li.mark').remove();
}
if (!this.bmNotes.innerText.includes((this.divider).slice('\n')[1])) {
this.bmNotes.innerHTML = this.makeNotes();
this.notes = this.makeNotes();
}
this.statusTags(document, this.words);
this.updateStorage(this.blurb, this.storageID, this.tags);
if (!this.isSeries) {
//this.buttonComment(this.settings.databaseInfo[2].enabled);
this.buttonKudos(this.settings.databaseInfo[3].enabled);
this.buttonSubscribe(this.settings.databaseInfo[5].enabled);
this.buttonTags(this.storageID, this.getBookmarkData());
this.buttonLastRead(this.getBookmarkData());
}
this.formNoteButtons(document, "#bookmark_notes", this.workID, this.descrip, this.summary, this.notes);
this.formTagButtons(document, this.workID);
}
buttonLastRead(bookmarkData) {
const footer = document.querySelector('div#feedback ul.actions');
let button = Object.assign(document.createElement('a'), {
id: `${this.id}-SaveChapter-btn`,
href: '#',
innerText: `🔖 Save Chapter`,
});
let container = Object.assign(document.createElement('li'));
container.append(button);
if (!this.isSeries) {
footer.append(container);
}
// Add Click Listeners
let genNote = this.makeNotes();
let workID = this.workID;
document.querySelectorAll(`#${this.id}-SaveChapter-btn`).forEach(button => {
button.addEventListener('click', (event) => {
event.preventDefault();
bookmarkData.notes = genNote;
DEBUG && console.log(`[Kat's Tweaks] Updating Last Read.`, bookmarkData.notes);
document.getElementById("bookmark_notes").innerHTML = bookmarkData.notes;
button.innerText = `🎉 Saved!`;
this.requestHandler(bookmarkData, workID, this.settings.showUpdatedBookmarks);
});
});
}
buttonComment(pushBM) {
let chapterID = getChapterID();
document.querySelectorAll(`input#comment_submit_for_${this.workID}, input#comment_submit_for_${chapterID}`).forEach(button => {
button.addEventListener('click', async (event) => {
event.preventDefault();
console.log(`[Kat's Tweaks] Adding tag: Commented`);
this.storage.addIdToCategory(`${this.id}-INFO-Commented`, this.storageID);
if (pushBM) {
document.querySelector('#bookmark_tag_string_autocomplete').value += `Commented, `;
this.tags.push('Commented');
await new Promise(res => setTimeout(res, 1000));
this.requestHandler(this.getBookmarkData(), this.workID, this.settings.showUpdatedBookmarks)
}
});
});
function getChapterID() {
if (document.URL.includes('chapters')) {
let id = document.URL.split('/')[6].split('#')[0].split('?')[0];
DEBUG && console.log(`[Kat's Tweaks] Chapter ID: ${id}`);
return id;
}
}
}
buttonKudos(pushBM) {
document.querySelectorAll(`#kudo_submit`).forEach(button => {
button.addEventListener('click', async (event) => {
event.preventDefault();
console.log(`[Kat's Tweaks] Adding tag: Kudosed`);
this.storage.addIdToCategory(`${this.id}-INFO-Kudosed`, this.storageID);
if (pushBM) {
document.querySelector('#bookmark_tag_string_autocomplete').value += `Kudosed, `;
this.tags.push('Kudosed');
await new Promise(res => setTimeout(res, 1000));
this.requestHandler(this.getBookmarkData(), this.workID, false)
}
});
});
}
buttonSubscribe(pushBM) {
document.querySelectorAll(`form#new_subscription`).forEach(button => {
button.addEventListener('click', async (event) => {
event.preventDefault();
console.log(`[Kat's Tweaks] Adding tag: Subscribed`);
this.storage.addIdToCategory(`${this.id}-INFO-Subscribed`, this.storageID);
if (pushBM) {
document.querySelector('#bookmark_tag_string_autocomplete').value += `Subscribed, `;
this.tags.push('Subscribed');
await new Promise(res => setTimeout(res, 1000));
this.requestHandler(this.getBookmarkData(), this.workID, this.settings.showUpdatedBookmarks)
}
});
});
}
// Add action buttons to the UI for each status
buttonTags(storageID, bookmarkData) {
const id = this.id;
const header = this.isSeries ? document.querySelector('div#main ul.navigation.actions') : document.querySelector('ul.work.navigation.actions');
const footer = document.querySelector('div#feedback ul.actions');
this.settings.databaseTags.forEach(({keyID, tagLabel, posLabel, negLabel, btnHeader, btnFooter}) => {
const isTagged = this.tags.includes(tagLabel);
if (btnHeader) { header.appendChild(makeButton()); }
if (btnFooter && !this.isSeries) { footer.append(makeButton()); }
document.querySelectorAll(`#${this.id}-TAGS-${keyID}-btn`).forEach(button => {
button.addEventListener('click', (event) => {
event.preventDefault();
this.handleTagButton(keyID, tagLabel, posLabel, negLabel, storageID, bookmarkData);
});
});
function makeButton() {
let button = Object.assign(document.createElement('a'), {
id: `${id}-TAGS-${keyID}-btn`,
href: '#',
innerText: `${isTagged ? negLabel : posLabel}`,
});
let container = Object.assign(document.createElement('li'));
container.append(button);
return container;
}
});
}
// Handle the action for adding/removing/deleting a bookmark tag
handleTagButton(keyID, tagLabel, posLabel, negLabel, storageID, bookmarkData) {
const buttons = document.querySelectorAll(`#${this.id}-TAGS-${keyID}-btn`);
// Disable the buttons and show loading state
buttons.forEach((btn) => {
btn.innerHTML = "Loading...";
btn.disabled = true;
});
try {
const isTagPresent = this.tags.includes(tagLabel);
// Toggle the bookmark tag and log the action
if (isTagPresent) {
console.log(`[Kat's Tweaks] Removing tag: ${tagLabel}`);
this.bmTags.value = `${this.bmTags.value.split(tagLabel)[0]}${this.bmTags.value.split(tagLabel)[1]}`;
this.tags.splice(this.tags.indexOf(tagLabel), 1);
this.storage.removeIdFromCategory(`${this.id}-TAGS-${keyID}`, storageID);
}
else {
console.log(`[Kat's Tweaks] Adding tag: ${tagLabel}`);
this.bmTags.value += `${tagLabel}, `;
this.tags.push(tagLabel);
this.storage.addIdToCategory(`${this.id}-TAGS-${keyID}`, storageID);
}
this.requestHandler(bookmarkData, storageID, this.settings.showUpdatedBookmarks)
// Update the labels for all buttons
buttons.forEach((btn) => {
btn.innerHTML = isTagPresent ? posLabel : negLabel;
});
}
catch (error) {
console.error(`[Kat's Tweaks] Error during bookmark operation:`, error);
buttons.forEach((btn) => {
btn.innerHTML = 'Error! Try Again';
});
}
finally {
buttons.forEach((btn) => {
btn.disabled = false;
});
}
}
getUserNotes() {
let note = (document.querySelector("div#bookmark-form textarea").innerText).split(this.divider)[0];
if (note.includes(this.divider.split('\n')[1])) {
console.warn(`[Kat's Tweaks] Something went wrong getting user note! Did the divider change?`);
DEBUG && console.log(`[Kat's Tweaks] Trying again. Old note: `, note);
let note2 = (document.querySelector("div#bookmark-form textarea").innerText).split((this.divider).split('\n')[1])[0];
if (!note2.length) {
console.warn(`[Kat's Tweaks] Failed to find user note. Regenerating default note.`)
return this.settings.defaultNote;
}
}
if (!note.length) {
return this.settings.defaultNote;
}
return note;
}
getTime() {
let currdate = new Date();
let dd = String(currdate.getDate()).padStart(2, '0');
let mm = String(currdate.getMonth() + 1).padStart(2, '0');
let yyyy = currdate.getFullYear();
let hh = String(currdate.getHours()).padStart(2, '0');
let mins = String(currdate.getMinutes()).padStart(2, '0');
let month = "";
if (mm == 0) { month = "January"; }
else if (mm == 1) { month = "February"; }
else if (mm == 2) { month = "March"; }
else if (mm == 3) { month = "April"; }
else if (mm == 4) { month = "May"; }
else if (mm == 5) { month = "June"; }
else if (mm == 6) { month = "July"; }
else if (mm == 7) { month = "August"; }
else if (mm == 8) { month = "September"; }
else if (mm == 9) { month = "October"; }
else if (mm == 10) { month = "November"; }
else if (mm == 11) { month = "December"; }
let timestamp = "";
if (this.settings.dateFormat == "Month/Year") { timestamp = `${mm}/${yyyy}`; }
else if (this.settings.dateFormat == "Day/Month/Year") { timestamp = `${dd}/${mm}/${yyyy}`; }
else if (this.settings.dateFormat == "Month/Day/Year") { timestamp = `${mm}/${dd}/${yyyy}`; }
else if (this.settings.dateFormat == "Worded Month/Year") { timestamp = `${month} ${yyyy}`; }
else if (this.settings.dateFormat == "Worded Day/Month/Year") { timestamp = `${dd} ${month} ${yyyy}`; }
else if (this.settings.dateFormat == "Worded Month/Day/Year") { timestamp = `${month} ${dd}, ${yyyy}`; }
else if (this.settings.dateFormat == "Exact Day/Month/Year") { timestamp = `${dd}/${mm}/${yyyy} [${hh}:${mins}]`; }
else if (this.settings.dateFormat == "Exact Month/Day/Year") { timestamp = `${mm}/${dd}/${yyyy} [${hh}:${mins}]`; }
else if (this.settings.dateFormat == "Exact Worded Day/Month/Year") { timestamp = `${dd} ${month} ${yyyy} [${hh}:${mins}]`; }
else if (this.settings.dateFormat == "Exact Worded Month/Day/Year") { timestamp = `${month} ${dd}, ${yyyy} [${hh}:${mins}]`; }
return timestamp;
}
getChapter() {
let nodes = document.querySelectorAll("div.preface.group h3.title a");
let chapter = (() => {
try {
let x = nodes[nodes.length-1];
return `<a href="${x.href}">${x.innerText}</a>`;
} catch (error) {
return "Oneshot";
}
})();
return chapter;
}
getSummary() {
const previousSummary = (document.querySelector("div#bookmark-form textarea").innerText).split(this.descrip)[1];
const summaryWork = this.isSeries ? document.querySelector("dl.series.meta.group blockquote.userstuff") : document.querySelector("div.preface.group div.summary.module blockquote.userstuff");
const summaryChap = this.isSeries ? "No Chapter Summary" : document.querySelector("div.chapter.preface.group div.summary.module blockquote.userstuff");
if (summaryWork && !(summaryWork == summaryChap)) {
DEBUG && console.log(`[Kat's Tweaks] Summary Found`);
return summaryWork.innerHTML;
}
else if (previousSummary) {
return previousSummary;
}
else {
return "No Summary Captured";
}
}
getPseudID() {
let singlePseud = document.querySelector('input#bookmark_pseud_id');
if (singlePseud) {
return singlePseud.value;
} else {
// If user has multiple pseuds - use the default one to create bookmark
let pseudSelect = document.querySelector('select#bookmark_pseud_id');
return pseudSelect.value;
}
}
}
class BookBlurb extends Bookmarking {
constructor(settings, blurb, moduleID) {
super(settings, moduleID);
this.blurb = blurb;
this.storageID = this.getStorageID(blurb);
if (this.storageID == '0') { return; }
this.workID = this.blurb.querySelector('h4.heading a').href.split('/').pop();
this.btnEdit = this.blurb.querySelector(`#bookmark_form_trigger_for_${this.workID}`);
this.tags = this.getTags();
this.checkBookmark();
this.pullFromStorage(this.blurb, this.settings);
// Load features for the form in list blurb
if (this.btnEdit) {
this.formFound(this.blurb);
}
}
pullFromStorage(blurb, settings) {
settings.databaseInfo.slice(2).forEach(({keyID}) => {
let ids = this.storage.getIdsFromCategory(`${this.id}-INFO-${keyID}`);
if (ids.includes(this.storageID)) {
blurb.classList.add(`${this.id}-INFO-${keyID}`);
}
});
this.settings.databaseTags.forEach(({keyID}) => {
let ids = this.storage.getIdsFromCategory(`${this.id}-TAGS-${keyID}`);
if (ids.includes(this.storageID)) {
blurb.classList.add(`${this.id}-TAGS-${keyID}`);
}
});
}
getTags() {
const tags = this.blurb.querySelectorAll("ul.meta.tags.commas a.tag") || "";
let x = [];
for (let i = 0; i < tags.length; i++) {
x.push(tags[i].innerText);
}
DEBUG && console.log(`[Kat's Tweaks] Tags Found for ${this.storageID}: `, x);
return x;
}
checkBookmark() {
// If a bookmark exists in the blurb, check if by user and check tags
let bookmarkBy = (() => {
try {
let by = this.blurb.querySelector("h5.byline.heading a").innerText;
return by;
} catch (error) {
return "";
}
})();
if ((bookmarkBy == this.username) && !this.isBookmarked) {
this.storage.addIdToCategory(`${this.id}-INFO-Bookmarked`, this.storageID);
this.blurb.classList.add(`${this.id}-INFO-Bookmarked`);
this.updateStorage(this.blurb, this.storageID, this.tags);
}
// New (Checked) & Bookmarked
this.isBookmarked = this.storage.getIdsFromCategory(`${this.id}-INFO-Bookmarked`).includes(this.storageID);
if (!this.isBookmarked && !this.storage.getIdsFromCategory(`${this.id}-INFO-Checked`).includes(this.storageID)) {
this.storage.addIdToCategory(`${this.id}-INFO-Checked`, this.storageID);
this.blurb.classList.add(`${this.id}-INFO-Checked`);
}
if (this.isBookmarked) {
this.storage.removeIdFromCategory(`${this.id}-INFO-Checked`, this.storageID);
this.blurb.classList.add(`${this.id}-INFO-Bookmarked`);
}
}
// Bookmark Form Functions
formFound(blurb) {
DEBUG && console.log(`[Kat's Tweaks] Found bookmark Edit button for ${this.workID}`);
const summary = blurb.querySelector('blockquote.userstuff.summary') ? blurb.querySelector('blockquote.userstuff.summary').innerHTML : "No Summary</details>";
this.btnEdit.addEventListener('click', async(event) => {
event.preventDefault();
if (document.getElementById(`${this.id}-tags-${this.workID}`)) {
DEBUG && console.log(`[Kat's Tweaks] Tag Container already exists!`);
return;
}
let form = blurb.querySelector('#bookmark-form');
while (!form) {
DEBUG && console.log(`[Kat's Tweaks] Waiting .25s`);
await new Promise(res => setTimeout(res, 250));
form = blurb.querySelector('#bookmark-form');
}
DEBUG && console.log(`[Kat's Tweaks] Found bookmark form`);
this.bookmarkID = document.querySelector('div#bookmark_form_placement form') ? document.querySelector('div#bookmark_form_placement form').getAttribute('action').split('/')[2] : null;
let notes = blurb.querySelector(`#bookmark_notes_${this.workID}`).innerHTML;
this.statusTags(blurb, this.getWordCount(blurb));
this.formNoteButtons(blurb, `#bookmark_notes_${this.workID}`, this.workID, this.descrip, summary, notes, true);
this.formTagButtons(blurb, this.workID);
});
}
getStorageID(work) {
if (work.querySelector('p.message')) {
return '0';
}
const link = work.querySelector('h4.heading a');
const ID = link.href.split('/').pop();
if (link.href.includes("series")) {
DEBUG && console.log(`[Kat's Tweaks] Series ${ID} found.`);
this.isSeries = true;
this.storage.addIdToCategory(`${this.id}-INFO-Series`, `${ID}S`);
return `${ID}S`;
}
return ID;
}
}
class BookSort {
constructor(settings, moduleID) {
this.id = moduleID;
this.settings = settings.bookmarking;
this.includedTags = [];
this.excludedTags = [];
this.container = this.createContainer();
this.handleFilter(this.settings, this.container[0], '#00ff0044', "other", this.includedTags);
this.handleFilter(this.settings, this.container[1], '#ff000044', "excluded", this.excludedTags);
}
handleFilter(settings, container, color, tagBox, array) {
let include = document.getElementById(`bookmark_search_${tagBox}_bookmark_tag_names_autocomplete`);
[settings.databaseTags, settings.databaseWord, settings.databaseInfo.slice(2)].forEach(database => {
database.forEach(({keyID, tagLabel}) => {
let button = Object.assign(document.createElement('input'), {
type: 'button',
id: `${this.id}-SORT-${keyID}-btn-${tagBox}`,
class: `${this.id}-sortButton`,
value: `${tagLabel}`,
});
container.append(button);
button.addEventListener('click', (event) => {
event.preventDefault();
const isIncluded = array.includes(`${tagLabel}`);
if (isIncluded) {
DEBUG && console.log(`[Kat's Tweaks] Removing ${tagLabel} from the input box.`);
if (include.value) {
if (include.value.includes(`${tagLabel}, `)) {
include.value = `${include.value.split(`${tagLabel}, `)[0]}${include.value.split(`${tagLabel}, `)[1]}`;
}
else {
include.value = `${include.value.split(`${tagLabel}`)[0]}${include.value.split(`${tagLabel}`)[1]}`;
}
}
array.splice(array.indexOf(tagLabel), 1);
}
else {
DEBUG && console.log(`[Kat's Tweaks] Adding ${tagLabel} to the input box.`);
if (include.value) {
include.value += `, ${tagLabel}`;
}
else {
include.value = `${tagLabel}`;
}
array.push(tagLabel);
}
button.style.backgroundColor = isIncluded ? 'initial' : color;
});
});
container.appendChild(document.createElement('hr'));
})
}
createContainer() {
let main = Object.assign(document.createElement('details'), {
id: `${this.id}-filterbox`,
});
main.appendChild(Object.assign(document.createElement('summary'), {
innerHTML: `<h4>Kat's Tweaks</h4><hr>`,
}));
let include = Object.assign(document.createElement('details'), {
id: `include`,
});
include.appendChild(Object.assign(document.createElement('summary'), {
innerHTML: `<h4>Include</h4><hr>`,
}));
let exclude = Object.assign(document.createElement('details'), {
id: `exclude`,
});
exclude.appendChild(Object.assign(document.createElement('summary'), {
innerHTML: `<h4>Exclude</h4><hr>`,
}));
main.appendChild(include);
//main.appendChild(document.createElement('hr'));
main.appendChild(exclude);
document.querySelector('dd.submit.actions').before(main);
return [include, exclude];
}
}
// Class for handling API requests
class RequestManager {
// Send an API request with the specified method
sendRequest(url, formData, headers, method = "POST") {
return fetch(url, {
method: method,
mode: "cors",
credentials: "include",
headers: headers,
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response;
})
.catch(error => {
DEBUG && console.error(`[Kat's Tweaks] Error during API request:`, error);
throw error;
});
}
// Create a bookmark for fanfic with given data
createBookmark(workId, authenticityToken, bookmarkData) {
const url = `https://archiveofourown.org/works/${workId}/bookmarks`;
const headers = this.getRequestHeaders();
const formData = this.createFormData(authenticityToken, bookmarkData);
DEBUG && console.info(`[Kat's Tweaks] Sending CREATE request for bookmark:`, {
url,
headers,
bookmarkData
});
return this.sendRequest(url, formData, headers)
.then(response => {
if (response.ok) {
const bookmarkId = response.url.split('/').pop();
console.info(`[Kat's Tweaks] Created bookmark ID:`, bookmarkId);
return bookmarkId;
} else {
throw new Error("Failed to create bookmark. Status: " + response.status);
}
})
.catch(error => {
DEBUG && console.error(`[Kat's Tweaks] Error creating bookmark:`, error);
throw error;
});
}
// Update a bookmark for fanfic with given data
updateBookmark(bookmarkId, authenticityToken, updatedData) {
const url = `https://archiveofourown.org//bookmarks/${bookmarkId}`;
const headers = this.getRequestHeaders();
const formData = this.createFormData(authenticityToken, updatedData, 'update');
DEBUG && console.info(`[Kat's Tweaks] Sending UPDATE request for bookmark:`, {
url,
headers,
updatedData
});
return this.sendRequest(url, formData, headers)
.then(data => {
console.info(`[Kat's Tweaks] Bookmark updated successfully:`, data);
})
.catch(error => {
console.error(`[Kat's Tweaks] Error updating bookmark:`, error);
});
}
// Retrieve the request headers
getRequestHeaders() {
const headers = {
"Accept": "text/html", // Accepted content type
"Cache-Control": "no-cache", // Prevent caching
"Pragma": "no-cache", // HTTP 1.0 compatibility
};
return headers;
}
// Create FormData for bookmarking actions based on action type
createFormData(authenticityToken, bookmarkData, type = 'create') {
const formData = new FormData();
// Append required data to FormData
formData.append('authenticity_token', authenticityToken);
formData.append("bookmark[pseud_id]", bookmarkData.pseudId);
formData.append("bookmark[bookmarker_notes]", bookmarkData.notes);
formData.append("bookmark[tag_string]", bookmarkData.tags.join(','));
formData.append("bookmark[collection_names]", bookmarkData.collections.join(','));
formData.append("bookmark[private]", +bookmarkData.isPrivate);
formData.append("bookmark[rec]", +bookmarkData.isRec);
// Append action type
formData.append("commit", type === 'create' ? "Create" : "Update");
if (type === 'update') {
formData.append("_method", "put");
}
DEBUG && console.log(`[Kat's Tweaks] FormData created successfully:`);
DEBUG && console.table(Array.from(formData.entries()));
return formData;
}
}
class StorageManager {
init(key) {
if (!localStorage.getItem(key)) {
DEBUG && console.log(`[Kat's Tweaks] Initilized Storage: ${key} | Previous Value: ${localStorage.getItem(key)}`)
localStorage.setItem(key, "")
}
}
// Store a value in local storage
setItem(key, value) {
localStorage.setItem(key, value);
}
// Retrieve a value from local storage
getItem(key) {
const value = localStorage.getItem(key);
return value;
}
// Add an ID to a specific category
addIdToCategory(category, id) {
const existingIds = this.getItem(category);
const idsArray = existingIds ? existingIds.split(',') : [];
if (!idsArray.includes(id)) {
idsArray.push(id);
this.setItem(category, idsArray.join(',')); // Update the category with new ID
DEBUG && console.debug(`[Kat's Tweaks] Added ID to category "${category}": ${id}`);
}
}
// Remove an ID from a specific category
removeIdFromCategory(category, id) {
const existingIds = this.getItem(category);
const idsArray = existingIds ? existingIds.split(',') : [];
const idx = idsArray.indexOf(id);
if (idx !== -1) {
idsArray.splice(idx, 1); // Remove the ID
this.setItem(category, idsArray.join(',')); // Update the category
DEBUG && console.debug(`[Kat's Tweaks] Removed ID from category "${category}": ${id}`);
}
}
// Get IDs from a specific category
getIdsFromCategory(category) {
const existingIds = this.getItem(category) || '';
const idsArray = existingIds.split(',');
DEBUG && console.debug(`[Kat's Tweaks] Retrieved IDs from category "${category}"`);
return idsArray;
}
}
class StyleManager {
static addStyle(debugID, css) {
const customStyle = document.createElement('style');
customStyle.id = 'KT';
customStyle.innerHTML = css;
document.head.appendChild(customStyle);
DEBUG && console.info(`[Kat's Tweaks] Custom style '${debugID}' added successfully`);
}
}
class Main {
constructor() {
this.settings = this.loadSettings();
this.loggedIn();
if (this.settings.bookmarking.enabled) {
let moduleID = "KT-BOOK";
console.info(`[Kat's Tweaks] Bookmarking | Initialized with:`, this.settings.bookmarking);
if (document.querySelector('form#bookmark-filters')) {
new BookSort(this.settings, moduleID);
}
if (document.getElementById('bookmark_tag_string_autocomplete')) {
new BookPage(this.settings, moduleID);
}
let blurbs = document.querySelectorAll('li.work.blurb, li.bookmark.blurb, li.series.blurb');
blurbs.forEach((blurb) => {
new BookBlurb(this.settings, blurb, moduleID);
});
StyleManager.addStyle('BOOK Default Style', `.${moduleID}-INFO-Bookmarked { border-right: 50px solid #ddd !important; } .${moduleID}-INFO-Checked { border-left: 5px solid #900 !important; } @media screen and (max-width: 62em) { .${moduleID}-INFO-Bookmarked { border-right: 20px solid #ddd !important; } }`);
StyleManager.addStyle('BOOK Reversi Overrides', ` .KT-reversi .${moduleID}-INFO-Bookmarked { border-right: 50px solid #555 !important; } .KT-reversi .${moduleID}-INFO-Checked { border-left: 5px solid #5998D6 !important; } @media screen and (max-width: 62em) { .KT-reversi .${moduleID}-INFO-Bookmarked { border-right: 20px solid #555 !important; } }`);
}
}
// Load settings from the storage or fallback to default ones
loadSettings() {
const startTime = performance.now();
let savedSettings = localStorage.getItem('KT-SavedSettings');
let settings = SETTINGS;
if (savedSettings) {
try {
let parse = JSON.parse(savedSettings);
DEBUG && console.log(`[Kat's Tweaks] Settings loaded successfully:`, savedSettings);
if (parse.bookmarking) {
settings = parse;
}
} catch (error) {
DEBUG && console.error(`[Kat's Tweaks] Error parsing settings: ${error}`);
}
} else {
DEBUG && console.warn(`[Kat's Tweaks] No saved settings found for Bookmarking, using default settings.`);
}
const endTime = performance.now();
DEBUG && console.log(`[Kat's Tweaks] Settings loaded in ${endTime - startTime} ms`);
return settings;
}
loggedIn() {
const userMenu = document.querySelector('ul.menu.dropdown-menu');
let foundUser = userMenu?.previousElementSibling?.getAttribute('href')?.split('/').pop() ?? '';
// if logged in
if (foundUser) {
DEBUG && console.log(`[Kat's Tweaks] Found Username: `, foundUser);
if (localStorage.getItem("KT-SavedUsername") !== foundUser) {
localStorage.setItem("KT-SavedUsername", foundUser);
}
}
// if not logged in, but remembers username
else if (!!localStorage.getItem("KT-SavedUsername")) {
console.info(`[Kat's Tweaks] Bookmarking | Didn't find username on page, saved username: `, localStorage.getItem("KT-SavedUsername"));
}
else {
let newUser = prompt(`[Kat's Tweaks]\nUsername is used to check for bookmarks and other functions.\n\nYour AO3 username:`);
localStorage.setItem('KT-SavedUsername', newUser);
}
}
}
new Main();