Greasy Fork

Novel Unreads Highlight

Custom colour for author/unreads on Kakuyomu / Narou / Alphapolis's favorite page

目前为 2024-05-30 提交的版本。查看 最新版本

// ==UserScript==
// @name           Novel Unreads Highlight
// @name:ja        未読小説をハイライトする
// @namespace      https://greasyfork.org/en/users/1264733
// @version        2024-05-30
// @description    Custom colour for author/unreads on Kakuyomu / Narou / Alphapolis's favorite page
// @description:ja アルファポリス・カクヨム・なろうの気に入りページに、作者・未読小説に色分けを追加する。
// @author         LE37
// @license        MIT
// @include        /^https:\/\/kakuyomu\.jp\/my\/antenna\/works/
// @include        /^https:\/\/kakuyomu\.jp\/works\/[0-9]+$/
// @include        /^https:\/\/kakuyomu\.jp\/works\/[0-9]+\/episodes\/[^\/]+$/
// @include        /^https:\/\/syosetu\.com\/favnovelmain\/list\//
// @include        /^https:\/\/ncode\.syosetu\.com\/[A-z0-9]+\/?$/
// @include        /^https:\/\/ncode\.syosetu\.com\/[A-z0-9]+\/[0-9]+\/?$/
// @include        /^https:\/\/www\.alphapolis\.co\.jp\/mypage\/notification\/index\/110000/
// @include        /^https:\/\/www\.alphapolis\.co\.jp\/novel\/[0-9]+/[0-9]+/episode/[0-9]+$/
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_registerMenuCommand
// ==/UserScript==

(()=>{
	'use strict';

	let gMk;
	switch (location.host) {
		case "kakuyomu.jp":
			gMk = "HUN_K";
			break;
		case "ncode.syosetu.com":
		case "syosetu.com":
			gMk = "HUN_N";
			break;
		case "www.alphapolis.co.jp":
			gMk = "HUN_A";
			break;
	}
	// GM menu
	GM_registerMenuCommand("CCheer", CCR);
	GM_registerMenuCommand("DAllEP", DAE);
	GM_registerMenuCommand("Author", ADA);
	GM_registerMenuCommand("Unread", SUN);
	GM_registerMenuCommand("Colour", SUC);
	// Read list
	const URD = GM_getValue(gMk);
	let tlo = URD ? URD : { ATC: false, SDB: false, FAC: "indigo", FCC: "orange", FUC: "red", FAU: 3, FAL:[], RRK:{} };
	let atc = tlo.ATC ? tlo.ATC : false;
	let sdb = tlo.SDB ? tlo.SDB : false;
	let tac = tlo.FAC ? tlo.FAC : "red";
	let tcc = tlo.FCC ? tlo.FCC : "deepskyblue";
	let tuc = tlo.FUC ? tlo.FUC : "orange";
	let tau = tlo.FAU;
	let rrk = tlo.RRK ? tlo.RRK : {};
	const tal = tlo.FAL;
	// Save list
	function USV() {
		tlo = { ATC: atc, SDB:sdb, FAC: tac, FCC: tcc, FUC: tuc, FAU: tau, FAL:tal, RRK:rrk };
		GM_setValue(gMk, tlo);
	}
	// Set fav author
	let sFa = false;
	// Set fav colour
	let sFc = false;
	const uRi = location.href;
	let fAuthor;

	const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
	if ( uRi.includes("/my/") || uRi.includes("/favnovelmain/") || uRi.includes("/mypage/") ) {
		FAV();
		CBT();
		CMU();
		if (uRi.includes("/favnovelmain/")) {
			CRH();
		}
	} else if (/^https:\/\/ncode\.syosetu\.com\/[A-z0-9]+\/?$/.test(uRi)) {
		const ckey = location.pathname.split("/")[1];
		if(Object.hasOwn(rrk, ckey)) {
			const tbtn = document.querySelector("div.novel_writername").appendChild(document.createElement("a"));
			tbtn.href = "https://ncode.syosetu.com/" + ckey + "/" + rrk[ckey].epi + "/";
			tbtn.textContent = "▶続きから読む";
		}
	} else if (/^https:\/\/kakuyomu\.jp\/works\/[0-9]+$/.test(uRi)) {
		// Kakuyomu add download all episodes button
		if (sdb && !document.querySelector("a.dAbtn")) {
			wait(2000).then(() => {
				const dAbtn = document.createElement("a");
				dAbtn.classList.add("dAbtn");
				dAbtn.style = "border: medium none; color: dodgerblue; margin: 2em;";
				dAbtn.textContent = "📥ダウンロード";
				dAbtn.addEventListener("click", (e) => {
					GEL(document.title.split("(")[0], uRi, "__NEXT_DATA__");
				});
				document.querySelector("div.partialGiftWidgetActivityName").appendChild(dAbtn);
			});
		}
	} else {
		EPI();
	}
	// Custom reading history
	function CRH() {
		if (!document.getElementById("rlst")) {
			const crl = document.querySelector("div.c-up-page-title").insertBefore(document.createElement("div"), document.querySelector("h2.c-up-page-title__text"));
			crl.id = "rlst";
			crl.style.marginBottom = "1em";
			crl.innerHTML = '<h2 id="crh" class="c-up-page-title__text" style="cursor: pointer;">閲覧履歴▼</h2><div id="rhd" style="margin: 1em 0 0 1em; display: none;"></div>';
			document.addEventListener("click", (e) => {
				if (e.target.classList.contains("drrk")) {
					const dkey = e.target.getAttribute("data");
					delete rrk[dkey];
					USV();
					CRH();
				}
			});
			document.getElementById("crh").addEventListener("click", (e) => {
				if (document.getElementById("rhd").style.display === "none") {
					document.getElementById("rhd").style.display = "";
					document.getElementById("crh").textContent = ">閲覧履歴▶";
				} else {
					document.getElementById("rhd").style.display = "none";
					document.getElementById("crh").textContent = ">閲覧履歴▼";
				}
			});
		}
		document.getElementById("rhd").innerHTML = "";
		for(const k in rrk) {
			document.getElementById("rhd").innerHTML += '<p style="font-size: 1em; line-height: 2em;"><span class="drrk" data="' + k + '" type="button" style="color: red; cursor: pointer;">✖</span><a href="https://ncode.syosetu.com/' + k + '/'+ rrk[k].epi + '/" style="margin-left: 1em;">'+ rrk[k].tit + '</a></p>';
		}
	}
	// Episode page
	function EPI() {
		// eCheer button;
		let eCb;
		let rMf = false;
		switch (gMk) {
			case "HUN_K":
				eCb = document.getElementById("episodeFooter-action-cheerButton");
				if ( document.getElementById("episodeFooter-action-cheerButton-cheer").classList.contains("isShown") && tal.some(name => document.title.includes('(' + name + ') -')) ) {
					rMf = true;
				}
				// Button download as txt
				TXT();
				break;
			case "HUN_N":
				eCb = document.querySelector("a.js-novelgood_change");
				if ( document.querySelector("div.is-empty") && tal.some(name => document.querySelector('div.contents1 a:nth-child(2)').textContent.includes(name)) ) {
					rMf = true;
				}
				// Auto siori/bookmark
				if (document.querySelector("li.bookmark_now")) {
					wait(Math.floor((Math.random() * (5000 - 2000 + 1)) + 2000)).then(() => {document.querySelector("li.bookmark_now>a").click();});
				}
				// Custom reading history
				const ckey = location.pathname.split("/")[1];
				const cepi = location.pathname.split("/")[2];
				if(!Object.hasOwn(rrk, ckey)) {
					rrk[ckey] = {"epi": null, "tit": null};
					let ctit = document.title.split(" - ")[0];
					if (ctit.length > 12) {
						ctit = ctit.slice(0, 12);
					}
					rrk[ckey].tit = ctit;
				}
				rrk[ckey].epi = cepi;
				USV();
				break;
			case "HUN_A":
				eCb = document.getElementById("contentMangaLikeBtnCircle");
				if ( !eCb.classList.contains("max") && tal.some(name => uRi.includes(name)) ) {
					rMf = true;
				}
				break;
		}
		const ioc = new IntersectionObserver((entries) => {
			if (entries[0].intersectionRatio <= 0) return;
			ioc.disconnect();
			if (rMf) {
				eCb.style.backgroundColor = tcc;
				if (atc) {
					if (gMk === "HUN_A") {
						// Randomnumber = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
						let x = 0;
						setInterval(function() {
							if (x < parseInt(eCb.getAttribute("data-content-like-limit"))) {
								//console.log(x);
								eCb.click();
							} else {
								return;
							}
							x++;
						}, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
					} else {
						eCb.click();
					}
					//console.log("===いいね===");
					wait(1500).then(() => {
						if (gMk === "HUN_K" && !document.getElementById("episodeFooter-action-cheerButton-cheer").classList.contains("isShown")
						|| gMk === "HUN_N" && !document.querySelector("div.is-empty")
						|| gMk === "HUN_A" && document.getElementById("contentMangaLikeBtnCircle").classList.contains("max") ) {
							eCb.style.backgroundColor = "";
						}
					});
				}
			}
		});
		ioc.observe(eCb);
	}
	// Favorite page
	function FAV() {
		let fNode, fUnreadCount;
		switch (gMk) {
			case "HUN_K":
				fAuthor = "p.widget-antennaList-author";
				fNode = "li.widget-antennaList-item";
				fUnreadCount = "li.widget-antennaList-unreadEpisodeCount";
				break;
			case "HUN_N":
				fAuthor = "div.p-up-bookmark-item__author>a";
				fNode = "li.p-up-bookmark-item";
				fUnreadCount = "span.p-up-bookmark-item__unread-num";
				break;
			case "HUN_A":
				fAuthor = "h2.title>a";
				fNode = "div.content-main";
				fUnreadCount = "a.disp-order";
				break;
		}
		const tNode = document.querySelectorAll(fNode);
		for(let i = 0; i < tNode.length; i++) {
			const fAuthorTag = tNode[i].querySelector(fAuthor);
			const fAuthorName = (gMk === "HUN_A") ? fAuthorTag.href.match(/\d+$/)[0] : fAuthorTag.textContent;
			fAuthorTag.style.color = CHK(fAuthorTag, fAuthorName) ? tac : "";
			const tUnreadCount = tNode[i].querySelector(fUnreadCount);
			const fCurrent = (gMk === "HUN_A") && tUnreadCount ? parseInt(tUnreadCount.textContent.match(/[0-9]+/)[0]) : 0;
			let tUnreadNum;
			if (tUnreadCount) {
				tUnreadNum = (gMk === "HUN_K") ? parseInt(tUnreadCount.textContent.match(/[0-9]+/)[0])
							: (gMk === "HUN_N") ? parseInt(tUnreadCount.textContent)
							: parseInt(tNode[i].querySelector("a.total").textContent.match(/[0-9]+/)[0]) - fCurrent;
			} else {
				tUnreadNum = 0;
			}
			const fUnreadColor = CHK(fAuthorTag, fAuthorName) ? tac
								: tUnreadCount && tUnreadNum > tau ? tuc
								: "";
			if (tUnreadCount) {
				if (gMk === "HUN_K") {
					const resume = tNode[i].querySelector("a.widget-antennaList-continueReading").href;
					tUnreadCount.innerHTML = '<a href="' + resume + '" style="color:' + fUnreadColor + ';">' + tUnreadCount.textContent + '</a>';
				} else if (gMk === "HUN_N") {
					tNode[i].querySelector("span.p-up-bookmark-item__unread").style.color = fUnreadColor;
				} else {
					tNode[i].querySelector(fUnreadCount).style.color = fUnreadColor;
				}
			} else {
				if (gMk === "HUN_A") {
					tNode[i].querySelector(fAuthor).style.color = tuc;
				}
			}
		}
	}

	// Check author name
	function CHK(elem, s) {
		const result = tal.some((v) => s === v);
		if (sFa) {
			elem.style.border = result ? "thin solid fuchsia" : "thin solid dodgerblue";
		} else {
			elem.style.border = "none";
		}
		return result;
	}
	// Auto cheering
	function CCR() {
		atc = !atc;
		const ttt = atc ? "On" : "Off";
		alert("AutoCheering is " + ttt);
		USV();
	}
	// Show download all button switch
	function DAE() {
		sdb = !sdb;
		const eee = sdb ? "On" : "Off";
		alert("Show download all button is " + eee);
		USV();
	}
	// Add fav author
	function ADA() {
		if (!sFa) {
			sFa = true;
			document.addEventListener("click", AAH, true);
		} else {
			sFa = false;
			document.removeEventListener("click", AAH, true);
			USV();
		}
		document.getElementById("cFbtn").textContent = sFa ? "💖" : "💟";
		FAV();
	}
	// Add author handler
	function AAH(e) {
		e.preventDefault();
		if (e.target.closest(fAuthor)) {
			if (gMk === "HUN_A") {
				UTL(e, e.target.href.match(/\d+$/)[0]);
			} else {
				UTL(e, e.target.textContent);
			}
			FAV();
		}
		return false;
	}
	// Update temp list
	function UTL(e, s) {
		const i = tal.findIndex((v) => v === s);
		if (i !== -1) {
			tal.splice(i,1);
		} else {
			tal.push(s);
		}
		//console.log(tal);
		return tal;
	}

	// Set unread number
	function SUN() {
		const t = parseInt(prompt("Enter unread counts", tau), 10);
		if (t >= 0) {
			tau = t;
		} else {
			tau = 3;
			alert("Invalid number, default[3] will be used.");
		}
		USV();
		FAV();
	}
	// Set unread colour
	function SUC() {
		if (!sFc) {
			sFc = true;
			document.addEventListener("click", CSH, true);
		} else {
			sFc = false;
			document.removeEventListener("click", CSH, true);
			USV();
		}
		document.getElementById("cMenu").style.display = sFc ? "" : "none";
	}
	let tCate;
	// Colour select handler
	function CSH(e) {
		e.preventDefault();
		if (e.target.classList.contains("customCates")) {
			e.target.textContent = "▣" + e.target.textContent.slice(1);
			const cca = document.getElementsByClassName("customCates");
			for(let i = 0; i < cca.length; i++) {
				cca[i].textContent = cca[i] === e.target ? "◉" + cca[i].textContent.slice(1) : "○" + cca[i].textContent.slice(1);
			}
			tCate = e.target.textContent.slice(1, 2);
			let tctc;
			switch (tCate) {
				case "0":
					tctc = tac;
					break;
				case "1":
					tctc = tcc;
					break;
				case "2":
					tctc = tuc;
					break;
			}
			const ccb = document.getElementsByClassName("customColour");
			for(let j = 0; j < ccb.length; j++) {
				ccb[j].textContent = ccb[j].style.color === tctc ? "▣ColourTest" : "▢ColourTest";
			}
		} else if (e.target.classList.contains("customColour")) {
			const cca = document.getElementsByClassName("customCates");
			for(let i = 0; i < cca.length; i++) {
				if (cca[i].textContent.slice(1, 2) === tCate) cca[i].style.color = e.target.style.color;
			}
			switch (tCate) {
				case "0":
					tac = e.target.style.color;
					break;
				case "1":
					tcc = e.target.style.color;
					break;
				case "2":
					tuc = e.target.style.color;
					break;
			}
			const ccb = document.getElementsByClassName("customColour");
			for(let j = 0; j < ccb.length; j++) {
				ccb[j].textContent = ccb[j] === e.target ? "▣ColourTest" : "▢ColourTest";
			}
			FAV();
		}
		return false;
	}

	// Create float button
	function CBT() {
		const cButton = document.body.appendChild(document.createElement("button"));
		// Button style
		cButton.id = "cFbtn";
		cButton.textContent = "💟";
		cButton.style = "position: fixed; bottom: 20%; right: 10%; width: 44px; height: 44px; z-index: 9999; font-size: 200%; opacity: 50%; cursor:pointer; border: none; padding: unset;";
		cButton.type = "button";
		cButton.addEventListener("click", (e) => {
			ADA();
		});
	}
	// Create colour list
	function CMU() {
		const cMenu = document.body.appendChild(document.createElement("div"));
		cMenu.id = 'cMenu';
		const cCates = [ 'AuthorColour', 'ButtonColour', 'UnreadColour' ];
		cCates.forEach((item, index) => {
			let tctc;
			switch (index) {
				case 0:
					tctc = tac;
					break;
				case 1:
					tctc = tcc;
					break;
				case 2:
					tctc = tuc;
					break;
			}
			const cMc = cMenu.appendChild(document.createElement("p"));
			cMc.classList.add("customCates");
			cMc.style = 'position: fixed; bottom: ' + (4+index)*5 + '%; right: 22%; z-index: 9999; color: ' + tctc + '; background-color: #393939; padding: 10px;';
			cMc.type = "button";
			cMc.textContent = "○" + index + ". " + item;
		});
		const colors = ['deepskyblue', 'blue', 'lime', 'green', 'fuchsia', 'indigo', 'orange', 'red'];
		colors.forEach((item, index) => {
			const cMb = cMenu.appendChild(document.createElement("p"));
			cMb.classList.add("customColour");
			cMb.style = 'position: fixed; bottom: ' + (4+index)*5 + '%; right: 55%; z-index: 9999; color: ' + item + '; background-color: #F3F3F3; padding: 10px;';
			cMb.type = "button";
			cMb.textContent = "▢ColourTest";
		});
		cMenu.style.display = "none";
	}
	// Todo: 1. one time download/save confirm
	// Kakuyomu get episodes list from novel page
	async function GEL(lt, url, elem) {
		const data = JSON.parse(document.getElementById(elem).innerHTML);
		const re = new RegExp("Episode:");
		const keys = data.props.pageProps.__APOLLO_STATE__;
		const min = parseInt(prompt("Enter a start episode number between 1 to final episode number", "1"));
		const max = parseInt(prompt("Enter a end episode number between start episode number to final episode number", "2"));
		let nlc = "";
		if (min >= 1 && min <= max) {
			let i = 1;
			for (let key in keys) {
				if (re.test(key)) {
					const ttit = i + ". " + keys[key].title;
					const turl = url + "/episodes/" + keys[key].id;
					// download episode base on input range
					if (i >= min && i <= max) {
						await wait(Math.floor((Math.random() * (10000 - 5000 + 1)) + 5000)).then(() => {
							nlc += ttit + ": " +turl + "\n";
							//console.log(nlc);
							GEC(ttit, turl);
						});
					}
					i++;
				}
			}
		} else {
			console.log("Invalid Inputs");
		}
		// Save novel episode list
		const a = document.createElement("a");
		a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(nlc);
		a.download = lt + '.txt';
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
	}

	// Kakuyomu get episode content
	function GEC(title, url) {
		fetch(url).then((response) => {
			if (response.ok) {
				return response.text();
			}
			throw new Error('Something went wrong');
		})
		.then((text) => {
			const doc = new DOMParser().parseFromString(text, 'text/html');
			const data = doc.querySelector("div.widget-episodeBody").textContent;
			const a = document.createElement("a");
			a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(data);
			a.download = title + '.txt';
			document.body.appendChild(a);
			a.click();
			document.body.removeChild(a);
		})
		.catch((error) => {
			console.log(error);
		});
	}
	// Kakuyomu download current epicode as txt
	function TXT() {
		const data = document.querySelector("div.widget-episodeBody").textContent;
		const dButton = document.getElementById("episodeFooter-action-cheerButtons").appendChild(document.createElement("a"));
		const title = document.querySelector("p.widget-episodeTitle") ? document.querySelector("p.widget-episodeTitle").textContent : document.title.replace(/\s/g,"").match(/[^-]+/);
		dButton.textContent = "📥ダウンロード";
		dButton.setAttribute('download', title + '.txt');
		dButton.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data));
	}
})();