// ==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 // @downloadURL none // ==/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 = '

閲覧履歴▼

'; 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 += '

'+ rrk[k].tit + '

'; } } // 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 = '' + tUnreadCount.textContent + ''; } 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)); } })();