AO3 Bookmark Improver

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
    });
})();