您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Allows for autofilled bookmark summary and tags on Ao3.
当前为
// ==UserScript== // @name Ao3 Auto Bookmarker // @description Allows for autofilled bookmark summary and tags on Ao3. // @namespace Ao3 // @match http*://archiveofourown.org/works/* // @match http*://archiveofourown.org/series/* // @grant none // @version 2.0 // @author Legovil // @license MIT // ==/UserScript== // Settings for the script, allowing customization of which features are enabled. const settings = { generateTitleNote: true, generateSummaryNote: true, checkRecBox: false, checkPrivateBox: false, getRating: true, getArchiveWarnings: true, getCategoryTags: true, getFandomTags: true, getRelationshipTags: true, getCharacterTags: true, getAdditionalTags: true, generateWordCountTag: true, appendToExistingNote: false, appendToExistingTags: true, usingAo3Extensions: false, }; // Word count boundaries for generating word count tags. const wordCountBounds = [1000, 5000, 10000, 50000, 100000, 500000]; // Enum for bookmark types. const BookmarkType = Object.freeze({ WORK: 'Work', SERIES: 'Series', }); (function() { 'use strict'; const bookmarkType = checkBookmarkType(window.location.href); if (!bookmarkType) { console.error('Bookmark type not found. Cancelling bookmark generation.'); return; } handleCheckBoxes(); setNotes(bookmarkType); setTags(bookmarkType); })(); /** * Sets notes based on bookmark type. * @param {string} bookmarkType - The type of the bookmark. */ function setNotes(bookmarkType) { const notesElement = document.getElementById('bookmark_notes'); if (!notesElement) { console.error('Notes element not found. Cancelling notes generation.'); return; } notesElement.value = generateNotes(bookmarkType, notesElement); } /** * Sets tags based on bookmark type. * @param {string} bookmarkType - The type of the bookmark. */ function setTags(bookmarkType) { const tagsElement = document.getElementById('bookmark_tag_string_autocomplete'); if (!tagsElement) { console.error('Tags input element not found. Cancelling bookmark tag generation.'); return; } tagsElement.value = generateTagsFromType(bookmarkType); } /** * Generates tags based on the bookmark type. * @param {string} bookmarkType - The type of the bookmark. * @return {string} - The generated tags. */ function generateTagsFromType(bookmarkType) { return (bookmarkType === BookmarkType.WORK) ? generateTags() : generateSeriesTags(); } /** * Checks the type of bookmark based on the URL. * @param {string} url The URL to check. * @return {string|null} The bookmark type or null if not found. */ function checkBookmarkType(url) { const bookmarkTypes = [ { type: '/works/', result: BookmarkType.WORK, message: 'Found Work Bookmark.' }, { type: '/series/', result: BookmarkType.SERIES, message: 'Found Series Bookmark.' } ]; const bookmarkType = bookmarkTypes.find(({ type }) => url.includes(type)); if (!bookmarkType) { return null; } console.log(bookmarkType.message); return bookmarkType.result; } /** * Generates notes for the bookmark. * @param {string} bookmarkType The type of bookmark. */ function generateNotes(bookmarkType, notesElement) { const notesArray = [ { setting: settings.generateTitleNote, note: generateTitleNote(bookmarkType) }, { setting: settings.generateSummaryNote, note: generateSummaryNote(bookmarkType) } ]; const notes = notesArray .filter(noteObj => noteObj.setting) .map(noteObj => noteObj.note) .join('\n\n'); // Append or replace existing notes based on settings. return settings.appendToExistingNote ? `${notesElement.value}\n\n${notes}` : notes; } /** * Generates the title note for the bookmark. * @param {string} bookmarkType The type of bookmark. * @return {string} The generated title note. */ function generateTitleNote(bookmarkType) { const queries = { WORK: { title: '.title.heading', author: '.byline.heading a' }, SERIES: { title: 'ul.series a', author: '.series.meta.group a' } }; const query = queries[bookmarkType]; if (!query) { console.warn(`Invalid bookmark type: ${bookmarkType}. Cancelling Title Note generation.`); return ''; } const { title: titleQuery, author: authorQuery } = query; const title = document.querySelector(titleQuery); if (!title) { console.warn('Title not found. Cancelling Title Note generation.'); return ''; } const author = document.querySelector(authorQuery); if (!author) { console.warn('Byline not found. Cancelling Title Note generation.'); return ''; } return `${title.innerHTML.link(window.location.href)} by ${author.outerHTML}.`; } /** * Generates the summary note for the bookmark. * @param {string} bookmarkType The type of bookmark. * @return {string} The generated summary note. */ function generateSummaryNote(bookmarkType) { const queries = { WORK: '.summary.module .userstuff', SERIES: '.series.meta.group .userstuff' }; const summaryQuery = queries[bookmarkType]; if (!summaryQuery) { console.warn(`Invalid bookmark type: ${bookmarkType}. Cancelling summary note generation.`); return ''; } const summary = document.querySelector(summaryQuery); if (!summary) { console.warn('No summary found. Cancelling summary note generation.'); return ''; } return `Summary: ${summary.innerText}`; } /* * Generates tags for the series bookmark. */ function generateSeriesTags() { const works = Array.from(document.querySelector(".series.work.index.group").children); console.log(works); if (!Array.isArray(works) || works.length === 0) { console.warn('No works found or invalid works array. Cancelling tag generation.'); return ''; } const tagTypes = [ { setting: settings.getArchiveWarnings, type: 'warnings' }, { setting: settings.getFandomTags, type: 'fandoms.heading' }, { setting: settings.getRelationshipTags, type: 'relationships' }, { setting: settings.getCharacterTags, type: 'characters' }, { setting: settings.getAdditionalTags, type: 'freeforms' } ]; const tags = works.flatMap(work => tagTypes .filter(tagType => tagType.setting) .flatMap(tagType => getTagsFromString(tagType.type, work)) ); if (settings.generateWordCountTag) { tags.push(generateWordCountTag()); } return Array.from(new Set(tags)).join(', '); } /** * Generates tags for the bookmark. */ function generateTags() { // Remove existing tags if the setting is not to append to them. if (!settings.appendToExistingTags) { document.querySelectorAll('.added.tag a').forEach(tagLink => tagLink.click()); } console.log('Tags input element found.'); const tagTypes = [ { setting: settings.getArchiveWarnings, type: 'warning.tags' }, { setting: settings.getCategoryTags, type: 'category.tags' }, { setting: settings.getFandomTags, type: 'fandom.tags' }, { setting: settings.getRelationshipTags, type: 'relationship.tags' }, { setting: settings.getCharacterTags, type: 'character.tags' }, { setting: settings.getAdditionalTags, type: 'freeform.tags' } ]; const tags = tagTypes .filter(tagType => tagType.setting) .map(tagType => getTagsFromString(tagType.type)) .flat(); if (settings.generateWordCountTag) { tags.push(generateWordCountTag()); } return tags.join(', '); } /** * Gets tags from a specific class name. * @param {string} tagClassName The class name to get tags from. * @return {string} The generated tags. */ function getTagsFromString(tagClassName, startNode = document) { const tagList = startNode.querySelectorAll(`.${tagClassName} .tag`); if (tagList.length === 0) { console.error(`Tags element not found. Cancelling ${tagClassName} generation.`); return ""; } return Array.from(tagList, tag => tag.text); } /** * Generates a word count tag based on the word count boundaries. * @return {string} The generated word count tag. */ function generateWordCountTag() { const index = settings.usingAo3Extensions ? 2 : 1; const wordCountElement = document.getElementsByClassName('words')[index]; if (!wordCountElement || wordCountElement.innerText === 'Words:') { console.error('Word count not found. Cancelling word count tag generation.'); return ''; } const wordCount = wordCountElement.innerText.replaceAll(',', '').replaceAll(' ', ''); let lowerBound = wordCountBounds[0]; if (wordCount < lowerBound) { return `< ${lowerBound}`; } for (const upperBound of wordCountBounds) { if (wordCount < upperBound) { return `${lowerBound} - ${upperBound}`; } lowerBound = upperBound; } return `> ${wordCountBounds[wordCountBounds.length - 1]}`; } /** * Handles the state of checkboxes based on settings. */ function handleCheckBoxes() { const checkBoxSettings = [ { setting: settings.checkRecBox, elementId: 'bookmark_rec', message: 'Checking rec box.' }, { setting: settings.checkPrivateBox, elementId: 'bookmark_private', message: 'Checking private box.' } ]; checkBoxSettings.forEach(({ elementId, setting, message }) => { const checkBox = document.getElementById(elementId); if (setting && checkBox) { console.log(message); checkBox.checked = true; } }); }