您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bookmark tracking, tagging, and more.
当前为
// ==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();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址