Greasy Fork 还支持 简体中文。

AO3: Go To to Latest Chapter

Adds a link to the chapter navigation bar to go to the latest chapter of a work. Alternative method is to add `/latest` to the end of an AO3 work URL. e.g. https://archiveofourown.org/works/{AO3_WORK_ID}/chapters/{AO3_CHAPTER_ID}/latest

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           AO3: Go To to Latest Chapter
// @namespace      https://github.com/w4tchdoge
// @version        1.1.1-20250816_185442
// @description    Adds a link to the chapter navigation bar to go to the latest chapter of a work. Alternative method is to add `/latest` to the end of an AO3 work URL. e.g. https://archiveofourown.org/works/{AO3_WORK_ID}/chapters/{AO3_CHAPTER_ID}/latest
// @author         w4tchdoge
// @homepage       https://github.com/w4tchdoge/MISC-UserScripts
// @match          *://archiveofourown.org/*works/*
// @match          *://archiveofourown.org/*works/*/latest
// @exclude        *://archiveofourown.org/*works/*/bookmarks
// @exclude        *://archiveofourown.org/*works/*/navigate
// @license        AGPL-3.0-or-later
// @run-at         document-start
// @history        1.1.1 — Make the "Latest ↠" button automatically take you to the start of the chapter text instead of the start of the webpage, making it consistent with the behaviour of the next & previous chapter buttons
// @history        1.1.0 — Use the chapter index dropdown to get the latest chapter ID when already viewing a multi chapter work instead of making an fetch request to the work's /navigate page
// @history        1.0.0 — Move the `latest_url` stuff to a function. Comment the code to the point where hopefully someone that doesn't know JS can understand what it's doing. Clean up old commented code that's no longer used. Add a description
// @history        0.0.1 — Initial commit
// ==/UserScript==

(async function () {
	`use strict`;

	// modified from https://stackoverflow.com/a/61511955/11750206
	function waitForElm(selector, search_root = document) {
		return new Promise(resolve => {
			if (search_root.querySelector(selector)) {
				return resolve(search_root.querySelector(selector));
			}

			const observer = new MutationObserver(mutations => {
				if (search_root.querySelector(selector)) {
					observer.disconnect();
					resolve(search_root.querySelector(selector));
				}
			});

			// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
			observer.observe(document.documentElement, {
				childList: true,
				subtree: true
			});
		});
	}

	// Gets URL of the latest chapter of a work
	// Input is the work ID as a string
	// Output is the latest URL as a URL object
	async function getLatestURL(ao3_work_id) {

		// Define the URL to be fetched
		const fetch_url = `https://archiveofourown.org/works/${ao3_work_id}/navigate`;
		// Fetch the URL as UTF-8 HTML and wait for the response from the server
		const fetch_resp = await fetch(fetch_url, { headers: { 'Content-Type': 'text/html; charset=utf-8', } });
		// Save the Response stream as a UTF-8 string
		const resp_text = await fetch_resp.text();

		// Create a new DOM parser
		const html_parser = new DOMParser();
		// Parse the response text as HTML and saves the output
		const html = html_parser.parseFromString(resp_text, `text/html`);

		// Search the parsed HTML of the navigate page for all links that go to a chapter
		const chapter_links_arr = Array.from(html.querySelectorAll(`#main .chapter.index.group > li > a[href^="/works"]`));
		// Get the href attribute of the last element in above array (which would be the latest chapter), and add the '#workskin' anchor that AO3 has for the buttons that take you to the next/prev chapter
		const latest_ch_href = `${chapter_links_arr.at(-1).getAttribute(`href`)}#workskin`;
		// Create a URL object from the latest chapter href
		const latest_url = new URL(latest_ch_href, `https://archiveofourown.org`);

		return latest_url;
	}


	// Get current page url as a URL object
	const curr_url = new URL(window.location);

	// Get AO3 work ID
	const work_id = (() => {

		// Split path of current URL on all forward slashes
		const pathname_segments = curr_url.pathname.split(`/`);
		// Get the index of the last element which is a string equal to 'works'
		// Add 1 to it to get the index of the work ID
		const wid_index = (pathname_segments.findLastIndex(elm => elm === `works`)) + 1;
		// Get work ID using the above index
		const wid = pathname_segments.at(wid_index);

		return wid;

	})();

	// Check if URL ends in latest
	if (curr_url.pathname.split(`/`).at(-1).toString().toLowerCase() == `latest`) { // If URL ends in latest

		// Stop further loading of webpage
		window.stop();

		// Get URL for the latest chapter
		const latest_url = await getLatestURL(work_id);

		// console.log(`latest`, curr_url, work_id, latest_url);

		// Redirect to latest chapter
		window.location.replace(latest_url);

	}
	else { // If URL doesn't end in latest

		// Get the current page's hostname
		const curr_pg_hostname = (new URL(window.location)).hostname;

		// Wait for the main content of the webpage to be loaded into the DOM
		const main = await waitForElm(`#main`);

		// Get the navbar where all the chapter navigation buttons are
		const work_nav_actions = main.querySelector(`.work.navigation.actions`);

		// Check to see if this is the latest chapter by checking for the presense of the 'Next Chapter →' button
		if (work_nav_actions.querySelector(`.chapter.next`)) { // If not on latest chapter

			// Get latest chapter ID & current work ID from DOM
			const
				latest_ch_id = work_nav_actions.querySelector(`#chapter_index form select#selected_id > option:last-of-type`).getAttribute(`value`),
				curr_work_id = work_nav_actions.querySelector(`#chapter_index form`).getAttribute(`action`).split(`/`).filter(e => e).at(1);

			// Construct URL for the latest chapter
			const latest_url = new URL(`works/${curr_work_id}/chapters/${latest_ch_id}#workskin`, `https://${curr_pg_hostname}`);

			// Get next chapter elements
			const next_chapter_elm_arr = Array.from(main.querySelectorAll(`li`)).filter(elm => elm.textContent === `Next Chapter →`);

			// console.log(next_chapter_elm_arr);

			// Create latest chapter element using the next chapter element as a base
			const latest_chapter_elm = ((input_elm) => {

				// Clone the node so the original stays unmodified
				let base_elm = input_elm.cloneNode(true);

				// Add the latest class to the button
				base_elm.classList.add(`latest`);

				// Get the actual link element present in the main element
				let link = base_elm.querySelector(`a`);
				// Set the href of the link element to the pathname of the URL to the latest chapter
				link.setAttribute(`href`, latest_url.href.toString().replace(latest_url.origin, ``));
				// Set the text of the link to indicate the button goes to the latest chapter
				link.textContent = `Latest ↠`;

				return base_elm;

			})(next_chapter_elm_arr.at(0));

			// console.log(latest_chapter_elm);

			// Add latest chapter elm after the next chapter elms
			next_chapter_elm_arr.forEach(element => {
				// Clone the latest chapter element while adding it so it can be put in two separate places
				element.after(latest_chapter_elm.cloneNode(true));
			});

		} else { console.log(`\nYou are already on the latest chapter.`); } // If on latest chapter

	}

})();