AniDB add-episode button

Adds a button next to MyList entries and episodes for adding a new episode.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        AniDB add-episode button
// @namespace   AquaWolfGuy
// @description Adds a button next to MyList entries and episodes for adding a new episode.
// @icon        https://static.anidb.net/css/icons/touch/apple-touch-icon.png
// @author      AquaWolfGuy
// @copyright   2018, AquaWolfGuy
// @license     GPL-3.0-only
// @match       *://anidb.net/*
// @version     1.0.6
// @grant       none
// ==/UserScript==


/////////////////
// Style sheet //
/////////////////

const style = document.createElement("style");
style.innerHTML = `
	#layout-main div.mylist_all table.animelist > tbody > tr > td.action {
		min-width: 8.5em;
	}
	@media screen and (min-width: 1640px) {
		#layout-main div.mylist_all table.animelist > tbody > tr > td.action {
			min-width: 14.5em;
		}
	}
	#layout-main div.anime_all div.episodes table.eplist td.action.episode {
		min-width: 13.5em;
	}
	.i_file_add::after {
		content: "\\f319";
		font-weight: normal;
	}
`;
document.head.appendChild(style);


////////////
// MyList //
////////////

const animelist = document.getElementById("animelist");
if (animelist !== null && document.body.classList.contains("mylist")) {

	new MutationObserver(preserveMyListButton).observe(animelist.tBodies[0], {childList: true});

	for (const row of animelist.querySelectorAll("#animelist>tbody>tr")) {
		addButtonToMyListEntry(row);
	}

	function addButtonToMyListEntry(row) {
		const aid = row.dataset.anidbAid;
		if (aid === undefined) {
			return;
		}
		const epsCell = row.getElementsByClassName("eps")[0];
		if (epsCell !== undefined) {
			const epsText = epsCell.textContent.trim();
			const epsParsed = /([0-9]+)\/([0-9]+)/.exec(epsText);
			if (epsParsed !== null && epsParsed[1] === epsParsed[2]) {
				return;
			}
		}
		const action = row.getElementsByClassName("action")[0];
		const button = document.createElement("a");
		button.className = "i_icon i_file_add";
		button.title = "add a file for the next episode";
		button.innerHTML = "<span>add file</span>";
		button.addEventListener("click", addFileByAid.bind(null, aid));
		button.addEventListener("auxclick", addFileByAid.bind(null, aid));
		button.addEventListener("mousedown", preventAutoScroll);
		action.appendChild(button);
	}

	function preserveMyListButton(mutationRecords, _mutationObserver) {
		for (const mutationRecord of mutationRecords) {
			for (const node of mutationRecord.addedNodes) {
				if (node instanceof Element && node.tagName === "TR") {
					addButtonToMyListEntry(node);
				}
			}
		}
	}

	function addFileByAid(aid, event) {
		if (!(event.button === 0 || event.button === 1)) {
			return;
		}
		const newTab = event.button === 1 || event.ctrlKey || event.shiftKey;

		const xhr = new XMLHttpRequest();
		xhr.responseType = "document";
		xhr.open("GET", "https://anidb.net/anime/" + aid);
		xhr.addEventListener("load", addFileXhrCallback.bind(null, xhr, newTab));
		xhr.addEventListener("error", xhrErrorCallback.bind(null, xhr));
		xhr.send();

		let loading = document.getElementById("loading");
		if (loading === null) {
			loading = document.createElement("div");
			loading.id = "loading";
			document.body.appendChild(loading);
		}
		loading.classList.add("active");

		event.preventDefault();
	}

	function addFileXhrCallback(xhr, newTab, _event) {
		const loading = document.getElementById("loading");
		if (loading !== null) {
			document.body.removeChild(loading);
		}

		if (xhr.status !== 200) {
			window.alert("Received \u201C" + xhr.status + " " + xhr.statusText + "\u201D response when loading episode list.");
			return;
		}

		let hasEps = false;
		let nextEpRow = null;
		for (const row of [...xhr.response.querySelectorAll("#eplist>tbody>tr")].reverse()) {
			const epNumber = row.getElementsByClassName("eid")[0].textContent.trim();
			if (!("0" <= epNumber[0] && epNumber[0] <= "9")) {
				 continue;
			}
			hasEps = true;
			const isAdded = row.getElementsByClassName("i_general_add").length === 0;
			if (isAdded) {
				break;
			}
			nextEpRow = row;
		}
		if (!hasEps) {
			window.alert("The anime entry does not have any normal episodes.");
			return;
		}
		if (nextEpRow === null) {
			window.alert("The last episode is in your MyList.");
			return;
		}
		const eid = nextEpRow.dataset.anidbEid;
		const uri = "https://anidb.net/file/add/?eid=" + eid;
		if (!newTab || window.open(uri) === null) {
			location.assign(uri);
		}
	}

	function xhrErrorCallback() {
		const loading = document.getElementById("loading");
		if (loading !== null) {
			document.body.removeChild(loading);
		}
		alert("Failed to load episodes list.");
	}

	function preventAutoScroll(event) {
		if (event.button === 1) {
			event.preventDefault();
		}
	}

}


////////////////
// Anime page //
////////////////

const eplist = document.getElementById("eplist");
if (eplist !== null) {

	for (const row of eplist.querySelectorAll("tbody>tr")) {
		addButtonToEpisodeEntry(row);
	}

	function addButtonToEpisodeEntry(row) {
		const eid = row.dataset.anidbEid;
		const action = row.querySelector(".action.episode");
		const button = document.createElement("a");
		button.className = "i_icon i_file_add";
		button.title = "add a new file";
		button.innerHTML = "<span>add new file</span>";
		button.href = "https://anidb.net/file/add/?eid=" + eid;
		action.insertBefore(button, action.firstElementChild);

		new MutationObserver(preserveEpisodeButton.bind(null, row)).observe(action, {childList: true});
	}

	function preserveEpisodeButton(row, mutationRecords, mutationObserver) {
		for (const mutationRecord of mutationRecords) {
			for (const node of mutationRecord.removedNodes) {
				if (node instanceof Element && node.classList.contains("i_file_add")) {
					mutationObserver.disconnect();
					addButtonToEpisodeEntry(row);
					return;
				}
			}
		}
	}

}