AO3 Bookmark Improver

Bookmark a work directly from any page and go back from bookmarking to browsing quickly

// ==UserScript==
// @name         AO3 Bookmark Improver
// @namespace    http://tampermonkey.net/
// @version      1.0
// @license      MIT
// @description  Bookmark a work directly from any page and go back from bookmarking to browsing quickly
// @author       sunkitten_shash
// @match        https://archiveofourown.org/*
// @match        http://archiveofourown.org/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.registerMenuCommand
// ==/UserScript==

// Much of the popup settings code heavily references BrickGrass' Blanket Permission Highlighter
// (https://github.com/BrickGrass/Blanket-Permission-Highlighter)

(function() {
    'use strict';

    // ---HTML AND CSS---

    // Styles for settings menu
    const css = `
    #bookmark-settings {
        position: fixed;
        z-index: 21;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgba(0, 0, 0, 0.4);
    }
    #bookmark-settings-content {
        background-color: #fff;
        color: #2a2a2a;
        margin: 10% auto;
        padding: 1em;
        width: 500px;
    }
    #bookmark-settings-content form {
        margin: 1em auto;
    }
    #bookmark-settings a {
        color: #111;
    }
    #bookmark-settings a:hover {
        color: #999;
    }
    #bookmark-settings .progress {
        color: green;
        font-size: .75rem;
    }
    #bookmark-settings button {
        background: #eee;
        color: #444;
        width: auto;
        font-size: 100%;
        line-height: 1.286;
        height: 1.286em;
        vertical-align: middle;
        display: inline-block;
        padding: 0.25em 0.75em;
        white-space: nowrap;
        overflow: visible;
        position: relative;
        text-decoration: none;
        border: 1px solid #bbb;
        border-bottom: 1px solid #aaa;
        background-image: -moz-linear-gradient(#fff 2%,#ddd 95%,#bbb 100%);
        background-image: -webkit-linear-gradient(#fff 2%,#ddd 95%,#bbb 100%);
        background-image: -o-linear-gradient(#fff 2%,#ddd 95%,#bbb 100%);
        background-image: -ms-linear-gradient(#fff 2%,#ddd 95%,#bbb 100%);
        background-image: linear-gradient(#fff 2%,#ddd 95%,#bbb 100%);
        border-radius: 0.25em;
        box-shadow: none;
    }
    @media only screen and (max-width: 625px) {
        #bookmark-settings-content {
            width: 80%;
        }
    }`;

    const bookmark_settings_html = `
    <div id="bookmark-settings">
        <div id="bookmark-settings-content">
            <h2>Ao3 Bookmark Improver Settings</h2>
            <br><br>
            <button id="bookmark-update">Update bookmarks list</button>
            <p>For if you have created a lot of bookmarks in a different browser or when not running this script</p><br>
            <button id="bookmark-clear">Clear bookmarks list</button>
            <p>Clear your entire bookmarks list, for if you have deleted a lot of bookmarks, switched users, or the list seems wrong</p>
            <button id="bookmark-settings-close">Close</button>
        </div>
    </div>`

    // ---GLOBAL VARIABLES---
    // variable that saves the scroll position on a page
    var scrollPos = 0;

    // cutoff variable for updating bookmarks
    let endOfPage = false;

    // abstracting out getting/setting the bookmark id lists into functions doesn't work
    // therefore their names are global variables for easier switching to new variables
    let workListName = "bookmarkWorkIds";
    let bookmarkListName = "bookmarkBookmarkIds";

    // ---SETTINGS PAGE CODE---
    GM.registerMenuCommand("AO3 Bookmark Improver Settings", function() {
        const settings_menu_exists = $("#bookmark-settings").length;
        if (settings_menu_exists) {
            console.log("settings already open");
            return;
        }

        $("body").prepend(bookmark_settings_html);

        $("#bookmark-update").click(updateBookmarkList);
        $("#bookmark-clear").click(clearBookmarks);

        $("#bookmark-settings-close").click(settings_close);
    });

    // close the settings dialog
    function settings_close() {
        $("#bookmark-settings").remove();

        window.location.reload();
    }

    // clear your entire bookmarks list
    async function clearBookmarks() {
        GM.setValue(workListName, []);
        GM.setValue(bookmarkListName, []);
        console.log("clear bookmarks");
        $("#bookmark-clear").after(`<p id="bookmark-clear-feedback" class="progress">Bookmark list cleared!</p>`);
        await new Promise(resolve => setTimeout(resolve, 5000));
        $("#bookmark-clear-feedback").remove();
    }

    // Goes through all of the user's bookmarks until it reaches the end or until there's < n (n being 10 here)
    // new bookmarks on a page, at which point it figures it's updated enough and terminates
    // This runs slowly. There's a 5 second pause in between requesting each page, and if there's an error, it waits
    // 5 minutes (since it assumes that means rate limiting)
    async function updateBookmarkList() {
        // get your userId from the little greeting in the corner
        let userId = $("#greeting > .user > .dropdown > .dropdown-toggle").attr("href").split('/')[2];
        let pageNum = 1;
        let newBookmarksNum = 0;
        endOfPage = false;

        let bookmarkWorkIds = await GM.getValue(workListName, []);
        let bookmarkBookmarkIds = await GM.getValue(bookmarkListName, []);
        console.log("initial bookmarks length: " + bookmarkWorkIds.length);

        $("button#bookmark-update").after(`<p id="bookmark-update-progress" class="progress">On page ${pageNum}...</p>`);
        // loop to go through all the bookmark pages that have new-to-us bookmarks
        while (!endOfPage) {
        //while (counter < 3) {
            // didn't feel like wrapping the entire thing in a timeout or making another function for it
            // bc I'm lazy, so here's just a timeout for 5 secs
            // this doesn't usually run into rate limiting for me
            //console.log({ counter });
            //counter++;
            await new Promise(resolve => setTimeout(resolve, 5000));
            try {
                await $.get(`https://archiveofourown.org/users/${userId}/bookmarks?page=${pageNum}`, (data) => {
                    console.log(pageNum);
                    /*if ($("#bookmark-update-progress").length > 0) {
                        console.log("already there");
                        console.log($("#bookmark-update-progress"));
                        $("#bookmark-update-progress").innerText = `page num: ${pageNum}`;
                    } else {
                        console.log("not there");
                        $("button#bookmark-update").after(`<p id="bookmark-update-progress" style="font-size:.75rem; color:green">page num: ${pageNum}</p>`);
                    }*/
                    $("#bookmark-update-progress").text(`On page ${pageNum}...`);
                    //$("button#bookmark-update").after(`<p id="bookmark-update-progress" style="font-size:.5rem">page num: ${pageNum}</p>`);
                    //console.log($("button#bookmark-update"));
                    let workLinks = $("li[role=article] > .header > .heading:first-child > a:not([href*=users]):not([href*=gifts])", data);
                    let bookmarkLinks = $(`li[role=article] > .own > .actions > li > a:contains("Edit")`, data);
                    if (workLinks.length === 0) {
                        console.log("no work links");
                        endOfPage = true;
                    } else {
                        let work_id = "";

                        // for each of the links in the page
                        // see if the work id is already in your bookmarks
                        // if it's not, add it to bookmarks
                        for (var i = 0, workLink; i < workLinks.length; i++) {
                            workLink = $(workLinks[i]);
                            work_id = workLink.attr("href").split('/')[2];
                            let bookmark_id = $(bookmarkLinks[i]).attr("href").split('/')[2];
                            // if this work_id is not in the bookmarks list
                            if (!bookmarkWorkIds.includes(work_id)) {
                                bookmarkWorkIds.push(work_id);
                                bookmarkBookmarkIds.push(bookmark_id);
                                newBookmarksNum++;
                            }
                        } // end for loop
                        console.log("new bookmarks num: " + newBookmarksNum);
                        // if you have more than 10 deleted or unrevelead bookmarks in a page, well, sucks to be you I guess
                        // otherwise good chance you've exhausted the bookmarks that you need to update, good job
                        if (newBookmarksNum < 10) {
                            console.log(`Terminating bookmarks list update on ${pageNum} with ${newBookmarksNum} new bookmarks`);
                            endOfPage = true;
                        }
                    } // endif
                    pageNum++;
                    newBookmarksNum = 0;
                }) // end the get thingy
            } // end try
            // if there's an error, wait five minutes cause it's probably rate limiting you
            catch (e) {
                console.log("Error requesting bookmarks page: " + e);
                await new Promise(resolve => setTimeout(resolve, 50000))
            }

            if (endOfPage) {
                console.log("Done updating bookmarks list");
                $("#bookmark-update-progress").text("Done updating bookmarks!");
                break;
            }
        } // end while loop
        GM.setValue(workListName, bookmarkWorkIds);
        GM.setValue(bookmarkListName, bookmarkBookmarkIds);
        await new Promise(resolve => setTimeout(resolve, 5000));
        $("#bookmark-update-progress").remove();
    }

    async function doFunctions(url) {
        if (url.includes("archiveofourown.org/bookmarks/")) {
            handleNewBookmark(url);
        }

        let userId = $("#greeting > .user > .dropdown > .dropdown-toggle").attr("href").split('/')[2];
        // this is the slightly nuclear option: if it's one of your pages, just don't show the bookmark button at all
        // it also eliminates all bookmark pages and lists of a user's collections (but not the list of works in the collection)
        // the reason we're eliminating all your pages is that they already have the edit navigation
        // section, so there's duplication
        if (!url.includes("/bookmarks") && !(url.includes("users") && url.includes("collection"))) {
            if (url.includes(userId)) {
                addYourSaveButtons(url);
            } else {
                addSaveButtons(url);
            }
        }
    }

    // adds buttons to create bookmarks on pages w/ your works
    async function addYourSaveButtons(url) {
        let bookmarkWorkIds = await GM.getValue(workListName, []);
        let bookmarkBookmarkIds = await GM.getValue(bookmarkListName, []);

        for (var i = 0, link, links = $("li[role=article] > .header > .heading:first-child > a:not([href*=users]):not([href*=gifts])"); i < links.length; i++) {
            let link = $(links[i]);
            let work_id = link.attr("href").split('/')[2];
            let btnText = 'Save';
            let urlModifier = `works/${work_id}/bookmarks/new`;
            if (bookmarkWorkIds.includes(work_id)) {
                let index = bookmarkWorkIds.indexOf(work_id);
                let bookmark_id = bookmarkBookmarkIds[index];
                btnText = 'Saved';
                urlModifier = `bookmarks/${bookmark_id}/edit`;
            }
            link.closest(".header")
                .nextAll(".stats")
                .after(`<ul class="actions" role="navigation"> <li> <a id="bookmark_form_trigger_` + work_id + `" data-remote="true" href="https://archiveofourown.org/` + urlModifier + `">${btnText}</a> </li> </ul>`);

            // when you click on this work's bookmark form trigger, it adds a div after it and loads in the bookmark form part
            // of the bookmark page
            $("#bookmark_form_trigger_" + work_id).click(function() {
                link.closest(".header")
                    .nextAll(".actions")
                    .after("<div id='bookmark_ext_div'></div>");
                $("#bookmark_ext_div").load(`https://archiveofourown.org/${urlModifier} #bookmark-form`, () => {
                    $("legend:contains('Bookmark')")
                        .after(`<p class="close actions"><a id="bookmark-form-close">×</a></p>`);

                    $("#bookmark-form-close").click(() => $("#bookmark_ext_div").remove());
                });
            });
        }

        $("a[id^=bookmark_form_trigger]").click(saveScrollPos);
    }

    // adds the buttons to create bookmarks
    async function addSaveButtons(url) {
        let bookmarkWorkIds = await GM.getValue(workListName, []);
        let bookmarkBookmarkIds = await GM.getValue(bookmarkListName, []);

        // go through all of the works on the page
        // and add the save/saved button and its functionality
        for (var i = 0, link, links = $("li[role=article]:not([id*=bookmark]) > .header > .heading:first-child > a:not([href*=users]):not([href*=gifts])"); i < links.length; i++) {
            let link = $(links[i]);
            let work_id = link.attr("href").split('/')[2];
            let btnText = 'Save';
            let urlModifier = `works/${work_id}/bookmarks/new`;
            // if this is already bookmarked
            // then we need to indicate that and put in the proper link to edit the bookmark
            if (bookmarkWorkIds.includes(work_id)) {
                let index = bookmarkWorkIds.indexOf(work_id);
                let bookmark_id = bookmarkBookmarkIds[index];
                btnText = 'Saved';
                urlModifier = `bookmarks/${bookmark_id}/edit`;
            }
            link.closest(".header")
                .nextAll(".stats")
                .after(`<ul class="actions" role="navigation"> <li> <a id="bookmark_form_trigger_` + work_id + `" data-remote="true" href="https://archiveofourown.org/` + urlModifier + `">${btnText}</a> </li> </ul>`);

            // when you click on this work's bookmark form trigger, it adds a div after it and loads in the bookmark form part
            // of the bookmark page
            $("#bookmark_form_trigger_" + work_id).click(function() {
                link.closest(".header")
                    .nextAll(".actions")
                    .after("<div id='bookmark_ext_div'></div>");
                $("#bookmark_ext_div").load(`https://archiveofourown.org/${urlModifier} #bookmark-form`, () => {
                    $("legend:contains('Bookmark')")
                        .after(`<p class="close actions"><a id="bookmark-form-close">×</a></p>`);

                    $("#bookmark-form-close").click(() => $("#bookmark_ext_div").remove());
                });
            });

        } // end for loop

        // after we've loaded in all our bookmark form triggers, set the onclick to save the scroll position for the back button
        $("a[id^=bookmark_form_trigger]").click(saveScrollPos);
    } // end addSaveButtons

    // on the dedicated bookmark page (usually when creating/updating a bookmark)
    // first of all, if it has the flash notice to show that it's a newly created bookmark
    // then get both the work id and the bookmark id and add them to the global list of created bookmarks
    // second, it also adds the back button to get back to where you were browsing
    async function handleNewBookmark(url) {
        let bookmarkWorkIds = await GM.getValue(workListName, []);
        let bookmarkBookmarkIds = await GM.getValue(bookmarkListName, []);

        // if this is a newly created bookmark
        if ($("div.flash.notice").text().includes("Bookmark was successfully created. It should appear in bookmark listings within the next few minutes.")) {
            let links = $("li[role=article] > .header > .heading:first-child > a:not([href*=users])");
            let link = $(links[0]);
            let work_id = link.attr("href").split('/')[2];
            let bookmark_id = url.split('/')[4];

            if (!bookmarkWorkIds.includes(work_id)) {
                console.log("this is a new work bookmarked!");
                bookmarkWorkIds.push(work_id);
                bookmarkBookmarkIds.push(bookmark_id);
                GM.setValue(workListName, bookmarkWorkIds);
                GM.setValue(bookmarkListName, bookmarkBookmarkIds);
            }
        }

        // back button
        addButton();
    }

    // add back button which redirects to the previous page
    // for "you created a new bookmark" or "you updated this bookmark" pages
    function addButton() {
        $(".bookmarks-show > .navigation").prepend(`<li><a href="${document.referrer}" id="backButton">← Go Back</a></li>`);
    }

    // scroll to saved position on page
    async function scrollToPos() {
        scrollPos = await GM.getValue("scroll");
        window.scrollTo(0, scrollPos);
    }

    // save the current position that the page is scrolled to
    function saveScrollPos() {
        scrollPos = window.scrollY;
        GM.setValue("scroll", scrollPos);
    }

    // when the page loads
    $(document).ready(function() {
        let url = window.location.href;

        // add custom CSS for settings menu
        let head = document.getElementsByTagName('head')[0];
        if (head) {
            let style = document.createElement('style');
            style.setAttribute('type', 'text/css');
            style.textContent = css;
            head.appendChild(style);
        }

        // if we're coming from a bookmarks page, scroll to your previous position on the page
        if (document.referrer.includes("archiveofourown.org/bookmarks/")) {
            scrollToPos();
        }

        doFunctions(url);
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址