你需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式
你需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式
你需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式
你需要先安装一款用户样式管理器扩展后才能安装此样式
你需要先安装一款用户样式管理器扩展后才能安装此样式
你需要先安装一款用户样式管理器扩展后才能安装此样式
(我已安装用户样式管理器,立即安装用户样式!)
- // ==UserScript==
- // @name futaba reverse res search
- // @namespace https://github.com/himuro-majika
- // @version 1.0.2
- // @description 被引用レスをポップアップ表示・自分の書き込みへのレスを通知しちゃう
- // @author himuro_majika
- // @license MIT
- // @match *://*.2chan.net/*/res/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_listValues
- // @grant GM_deleteValue
- // @grant GM_notification
- // ==/UserScript==
-
- (() => {
- 'use strict';
-
- // ====オプション====
- const USE_NOTIFICATION = true; //自分の書き込みに新着レスが有ったときに通知を表示する(true/false)
- const NOTIFICATION_TIMEOUT = 5000; //通知の表示時間(ms)
- const MAX_COMMENT_HISTORY_THREAD = 100; //最大レス履歴保存数(スレ)
- const MARKER_CHAR = "★"; //自分の書き込みに付けるマーカーの文字(任意:絵文字も可)
- // ================
-
- const script_title = "GM_FRRS";
- let qtlist = [];
- let popupOpenTimer;
- let popupCloseTimer;
- let url;
- let isPosted = false;
- let commentHistoryList;
- const startTime = new Date().getTime(); //count parsing time
- init();
-
- function init() {
- url = getUrl();
- checkLoading();
- makeSelfCommentPicker();
- observeInserted();
- setOnSubmitEvent();
- }
-
- function initParse() {
- searchSelfComment();
- searchQuotedRes();
- addCounter();
- console.log(script_title + ' - Parsing: ' + ((new Date()).getTime() - startTime) + 'msec'); //log parsing time
- }
-
- function getUrl() {
- return location.href.match(/^.+:\/\/(.+)/)[1];
- }
-
- function checkLoading() {
- let loadingTimer = setInterval(() => {
- if (!document.getElementById("futakuro-loading")) {
- initParse();
- clearInterval(loadingTimer);
- }
- }, 100)
- }
-
- function getThreImgSrc() {
- let threimg = document.querySelector("#master img");
- if (!threimg) threimg = document.querySelector(".thre > a > img");
- return threimg.src
- }
-
- function getQuotedRes() {
- return document.querySelectorAll(".thre td blockquote font[color='#789922']");
- }
-
- function searchQuotedRes(quote) {
- if (arguments.length == 0) {
- quote = getQuotedRes();
- }
- let bqs = document.querySelectorAll(".thre blockquote");
- quote.forEach(item => {
- let qtnum = getResNoFromTdChild(item.parentNode);
- let qtsrcnum = _searchQtSrc(item, qtnum, bqs);
- let qtitemindex = qtlist.findIndex(item => item.qtsrcnum == qtsrcnum);
-
- if (qtitemindex !== -1 && qtlist[qtitemindex].qtres.findIndex(item => item == qtnum) !== -1) return
-
- if (qtitemindex !== -1) {
- qtlist[qtitemindex].qtres.push(qtnum);
- } else {
- let qtres = [];
- qtres.push(qtnum);
- let objqt = {
- "qtsrcnum": qtsrcnum,
- "qtres": qtres
- }
- qtlist.push(objqt)
- }
- if (commentHistoryList && commentHistoryList.length > 0) {
- commentHistoryList.forEach(com => {
- if (com.resno == qtsrcnum) {
- highlightResponse(item);
- if (arguments.length > 0) {
- let text = "";
- item.parentNode.childNodes.forEach(node => {
- if (node.nodeName == "#text") {
- text += node.textContent + "\n";
- }
- });
- showNotification(text);
- }
- }
- })
- }
- });
- }
-
- function _searchQtSrc(qt, qtnum, bqs) {
- let qtText = qt.innerText.substr(1).trim();
- let qtsrcnum = "0";
-
- // レスナンバー(No.)
- // /^ *(No)?\.?[0-9]+ *$/
- let matchResNo = qtText.match(/^\s*No\.(\d+)\s*$/);
- if (matchResNo && document.getElementById("delcheck" + matchResNo[1])) {
- qtsrcnum = matchResNo[1];
- return qtsrcnum;
- }
-
- // 画像ファイル名
- // /^[0-9]+\.(jpg|png|gif|webm|mp4|webp)$/
- let matchFileName = qtText.match(/^\s*(\d+\.(jpg|png|gif|webm|mp4|webp))\s*$/);
- if (matchFileName) {
- let matchFileEle = document.querySelector('.thre a[href$="' + matchFileName[1] + '"]');
- if (matchFileEle) {
- qtsrcnum = getResNoFromTdChild(matchFileEle);
- return qtsrcnum;
- }
- }
-
- // レス本文
- if (qtText.substr(0,1) == ">") return qtsrcnum;
- const qtresnum = parseInt(qt.parentNode.parentNode.querySelector(".rsc").textContent);
- for (let i = qtresnum - 1; i > 0; i--) {
- let t = "";
- bqs[i].childNodes.forEach(node => {
- if (node.nodeName == "#text") {
- t += node.textContent;
- }
- });
- if (t.indexOf(qtText) >= 0) {
- qtsrcnum = getResNoFromTdChild(bqs[i].parentNode);
- if (qtsrcnum == qtnum) qtsrcnum = "0";
- break;
- }
- }
-
- return qtsrcnum;
- }
-
- // 被引用数の表示
- function addCounter() {
- let existedCounter = document.querySelectorAll("." + script_title + "_Counter");
- existedCounter.forEach(item => {
- item.remove();
- })
- let cno = document.querySelectorAll(".cno");
- cno.forEach(no => {
- let num = no.textContent.match(/\d+$/);
- let qtindex = qtlist.findIndex(item => item.qtsrcnum == num);
- if (qtindex == -1) return;
- let qtcount = qtlist[qtindex].qtres.length;
- const counter = document.createElement("a");
-
- counter.innerText = qtcount + "レス";
- counter.classList.add(script_title + "_Counter");
- counter.style.color = "#117743";
- counter.style.marginLeft = "0.5em";
- counter.setAttribute(script_title + "_num", num);
-
-
- counter.addEventListener("mouseenter", popupQuoteRes);
- counter.addEventListener("mouseleave", removePopup);
- no.parentNode.insertBefore(counter, no);
- });
- }
-
- // 被引用レスのポップアップ
- function popupQuoteRes(e) {
- if (!this.closest("." + script_title + "_popup")) {
- removePopupAll();
- }
- clearTimeout(popupCloseTimer);
- clearTimeout(popupOpenTimer);
- popupOpenTimer = setTimeout(() => {
- let srcnum = this.getAttribute(script_title + "_num");
- let qtitem = qtlist.find(item => item.qtsrcnum == srcnum);
- let qtres = qtitem.qtres;
- let resListContainer = makePopupContainer();
-
- let xpos = this.getBoundingClientRect().left + window.scrollX - 20;
- let ypos = this.getBoundingClientRect().bottom + window.scrollY;
- resListContainer.style.top = ypos.toString() + "px";
- if (xpos + 500 > window.innerWidth) {
- resListContainer.style.right = "20px";
- } else {
- resListContainer.style.left = xpos.toString() + "px";
- }
-
- let resListTable = setPopupContent(qtres);
-
- resListContainer.appendChild(resListTable);
- document.querySelector("div.thre").appendChild(resListContainer);
- }, 200);
- }
-
- function setPopupContent(resnolist) {
- if (resnolist.length == 0) {
- let noEle = document.createElement("div");
- noEle.textContent = "該当レスがありません。";
- return noEle;
- }
- let resListTable = document.createElement("table");
- let resListTbody = document.createElement("tbody");
-
- resnolist.forEach(res => {
- let td = document.getElementById("delcheck" + res).parentNode.cloneNode(true);
- let rsc = td.querySelector(".rsc");
- rsc.classList.add("qtjmp");
- let jumpid = rsc.id;
- rsc.removeAttribute("id");
- rsc.addEventListener("click", () => {
- let jumptarget = document.getElementById(jumpid).parentNode;
- window.scroll(0, jumptarget.getBoundingClientRect().top + window.pageYOffset);
- removePopup();
- });
-
- const counter = td.querySelector("." + script_title + "_Counter");
- if (counter) {
- counter.addEventListener("mouseenter", popupQuoteRes);
- counter.addEventListener("mouseleave", removePopup);
- }
-
- const futakuro_resno = td.querySelector(".res_no");
- if (futakuro_resno) futakuro_resno.style.display = "none";
-
- if (checkAkahukuEnabled()) {
- // resListContainer.setAttribute("__akahuku_reply_popup_index", resno);
- let gtdiv = document.createElement("div");
- gtdiv.classList.add("akahuku_popup_content_blockquote");
- let bq = td.querySelector("blockquote");
- gtdiv.innerHTML = bq.innerHTML;
- bq.remove();
- td.appendChild(gtdiv);
- }
- let resListTr = document.createElement("tr");
- resListTr.appendChild(td);
- resListTbody.appendChild(resListTr);
- })
- resListTable.appendChild(resListTbody);
-
- return resListTable;
- }
-
- function makePopupContainer() {
- let container = document.createElement("div");
- container.classList.add(script_title + "_popup");
- if (checkAkahukuEnabled()) {
- container.classList.add("akahuku_reply_popup");
- } else {
- container.style.backgroundColor = "#F0E0D6";
- container.style.boxShadow = "1px 1px 3px 1px #777";
- container.style.borderRadius = "5px";
- container.style.fontSize = "0.85em";
- }
- container.style.position = "absolute";
- container.style.zIndex = 302;
- container.addEventListener("mouseenter", () => {
- clearTimeout(popupCloseTimer);
- });
- container.addEventListener("mouseleave", removePopup);
- return container;
- }
-
- function removePopup(souceEl) {
- clearTimeout(popupCloseTimer);
- clearTimeout(popupOpenTimer);
- if (!souceEl) return;
- if (!souceEl.relatedTarget) return;
- if (souceEl.srcElement.className == "GM_FRRS_Counter" &&
- souceEl.relatedTarget.closest(".GM_FRRS_popup")) return;
- if (souceEl.srcElement.classList.contains("GM_FRRS_popup") &&
- souceEl.relatedTarget.closest(".GM_FRRS_popup")) {
- removeForwardPopupSibling(souceEl.relatedTarget.closest(".GM_FRRS_popup"));
- return;
- }
- popupCloseTimer = setTimeout(() => {
- removePopupAll();
- }, 500);
- }
-
- function removePopupAll() {
- let popup = document.querySelectorAll("." + script_title + "_popup");
- if (!popup) return;
- popup.forEach(p => {
- p.remove();
- })
- }
-
- function removeForwardPopupSibling(ele) {
- if (!ele.nextElementSibling) return;
- let nextsibling = ele.nextElementSibling;
- if (nextsibling.classList.contains("GM_FRRS_popup")) {
- nextsibling.remove();
- } else {
- return;
- }
- removeForwardPopupSibling(ele);
- }
-
- // 続きを読むで挿入される要素を監視
- function observeInserted() {
- let target = document.querySelector(".thre");
- if (!target) return;
- let observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (!mutation.addedNodes.length) return;
- if (mutation.addedNodes[0].className == script_title + "_popup") return;
- if (mutation.addedNodes[0].querySelector(".rtd")) {
- refreshCounter(mutation.addedNodes);
- }
- });
- if (isPosted) {
- isPosted = false;
- selfComment(mutations);
- }
- });
- observer.observe(target, {
- childList: true
- });
- }
-
- function refreshCounter(nodes) {
- let qt = nodes[0].querySelectorAll(".thre td blockquote font[color='#789922']");
- if (!qt.length) return;
- searchQuotedRes(qt);
- addCounter();
- }
-
- // レス投稿時のイベント設定
- function setOnSubmitEvent() {
- let formEle = document.getElementById("fm");
-
- formEle.addEventListener("submit", () => {
- onCommentSend();
- });
- let button = formEle.querySelector("input[type='submit'");
- if (button) {
- button.addEventListener("click", () => {
- onCommentSend();
- });
- }
- }
-
- function onCommentSend() {
- if (isPosted) return;
- isPosted = true;
- let textbody = document.getElementById("ftxa").value.trim();
- storeCommentHistory(textbody);
- }
-
- // 書き込み履歴の保存
- function storeCommentHistory(commentText) {
- if (typeof(commentText) !== "string") return;
-
- commentHistoryList = getCommentHistory();
-
- let comment = {
- "comment": commentText,
- "resno": ""
- }
- if (!commentHistoryList) {
- commentHistoryList = [];
- }
- commentHistoryList.push(comment);
-
- setCommentHistory(commentHistoryList);
- console.log(commentHistoryList);
- setTimeout(() => {
- expireCommentHistory();
- }, 5000);
- }
-
- function getCommentHistory() {
- let commentHistory = getValue(url);
- return commentHistory;
- }
-
- function setCommentHistory(commenthistory) {
- setValue(url, commenthistory);
- return;
- }
-
- function searchSelfComment() {
- let listUpdatedFlag = false;
- commentHistoryList = getCommentHistory();
- if (!commentHistoryList) return;
- commentHistoryList.forEach(element => {
- let elresno = element.resno;
- if (elresno !== "") {
- let sd = document.getElementById("sd" + elresno);
- if (!sd) return;
- highlightOwnRes(sd);
- }
- let comment = element.comment;
- if (elresno == "" && comment) {
- console.log(comment);
- let bq = document.querySelectorAll(".thre .rtd blockquote");
- bq.forEach(item => {
- let bqtext = item.innerText;
- if (comment == bqtext) {
- let bqresno = getResNoFromTdChild(item);
- let itemsd = document.getElementById("sd" + bqresno);
- if (!itemsd) return;
- highlightOwnRes(itemsd);
- element.resno = bqresno;
- listUpdatedFlag = true;
- // console.log(bqresno);
- }
- })
- }
- });
- if (listUpdatedFlag) {
- setCommentHistory(commentHistoryList);
- }
- // console.log(qtlist);
- }
-
- function selfComment(mutations) {
- commentHistoryList = getCommentHistory();
- if (!commentHistoryList) return;
- if (commentHistoryList[commentHistoryList.length - 1].resno !== "") return;
- let latestStoredComment = commentHistoryList[commentHistoryList.length - 1].comment;
- let hitres;
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length && mutation.addedNodes[0].querySelector(".rtd")) {
- let table = mutation.addedNodes[0];
- let bq = table.querySelector("blockquote");
- if (!bq) return;
- let bqtext = bq.innerText;
- if (latestStoredComment === "") {
- if (bqtext !== "キタ━━━(゚∀゚)━━━!!" &&
- bqtext !== "キタ━━━━━━(゚∀゚)━━━━━━ !!!!!" &&
- bqtext !== "本文無し") {
- return;
- }
- } else if (bqtext !== latestStoredComment) {
- return;
- }
- commentHistoryList[commentHistoryList.length - 1].resno = getResNoFromTdChild(bq);
- hitres = table;
- }
- });
- if (!hitres) return;
- // console.log(hitres);
- setCommentHistory(commentHistoryList);
- let sd = hitres.querySelector(".sod");
- highlightOwnRes(sd);
- // console.log(commentHistoryList);
- }
-
- function highlightOwnRes(node) {
- if (node.parentNode.querySelector("." + script_title + "_own_res")) return;
- let marker = document.createElement("span");
-
- marker.innerText = MARKER_CHAR;
- marker.classList.add(script_title + "_own_res");
- // marker.style.fontWeight = "bold";
- marker.style.color = "#117743";
- marker.style.cursor = "pointer";
- marker.addEventListener("click", () => {
- let selfResList = document.querySelectorAll("." + script_title + "_own_res");
- popupSelfCommentList(selfResList);
- });
- let rsc = node.parentNode.querySelector(".rsc");
- let futakuroResNo = node.parentNode.querySelector(".res_no");
- if (futakuroResNo) {
- rsc = futakuroResNo;
- }
- rsc.style.color = "#1b54ff";
- rsc.style.fontWeight = "bold";
- rsc.style.fontSize = "smaller";
- rsc.style.cursor = "pointer";
- rsc.addEventListener("click", () => {
- let selfResList = document.querySelectorAll("." + script_title + "_own_res");
- popupSelfCommentList(selfResList);
- });
- node.parentNode.insertBefore(marker, node.previousSibling);
- }
-
- // 自分の書き込みへのレスをハイライト
- function highlightResponse(bq) {
- bq.parentNode.classList.add(script_title + "_response");
- let rsc = bq.parentNode.parentNode.querySelector(".rsc");
- let futakuroResNo = bq.parentNode.parentNode.querySelector(".res_no");
- if (futakuroResNo) {
- rsc = futakuroResNo;
- }
- rsc.style.color = "#ff0078";
- rsc.style.fontWeight = "bold";
- rsc.style.fontSize = "smaller";
- rsc.style.cursor = "pointer";
- rsc.addEventListener("click", () => {
- let selfResList = document.querySelectorAll("." + script_title + "_response");
- document.getElementById(script_title + "_new_comment").style.color = "#0040ee";
- popupSelfCommentList(selfResList);
- });
- }
-
- // 自分の書き込み一覧ポップアップ
- function makeSelfCommentPicker() {
- let commentPickerContainer = document.createElement("div");
- commentPickerContainer.id = script_title + "_comment_picker_container";
- commentPickerContainer.style.fontSize = "9pt";
-
- let commentPicker = document.createElement("a");
- commentPicker.id = script_title + "_self_comment_picker";
- commentPicker.textContent = "📑[自分の書き込み]";
- commentPicker.style.color = "#0040ee";
- commentPicker.style.cursor = "pointer";
- commentPicker.addEventListener("click", () => {
- let selfResList = document.querySelectorAll("." + script_title + "_own_res");
- popupSelfCommentList(selfResList);
- });
- let newComment = document.createElement("a");
- newComment.id = script_title + "_new_comment";
- newComment.textContent = "[書き込みへのレス]";
- newComment.style.cursor = "pointer";
- newComment.style.color = "#0040ee";
- newComment.addEventListener("click", () => {
- let selfResList = document.querySelectorAll("." + script_title + "_response");
- document.getElementById(script_title + "_new_comment").style.color = "#0040ee";
- popupSelfCommentList(selfResList);
- });
- commentPickerContainer.appendChild(commentPicker);
- commentPickerContainer.appendChild(newComment);
-
- let pwd = document.getElementById("usercounter");
- pwd.parentNode.insertBefore(commentPickerContainer, pwd);
- }
-
- function popupSelfCommentList(selfResList) {
- let popup = document.getElementById(script_title + "_own_res_popup");
- if (popup) {
- popup.remove();
- return;
- }
- let container = makeSelfCommentListContainer();
- let selfResNoList = [];
- selfResList.forEach(res => {
- selfResNoList.push(getResNoFromTdChild(res));
- });
- let qttable = setPopupContent(selfResNoList);
- container.appendChild(qttable);
- document.querySelector("html body").appendChild(container);
- }
-
- function makeSelfCommentListContainer() {
- let container = document.createElement("div");
- container.id = script_title + "_own_res_popup";
- if (checkAkahukuEnabled()) {
- container.classList.add("akahuku_reply_popup");
- } else {
- container.style.boxShadow = "1px 1px 3px 1px #777";
- container.style.borderRadius = "5px";
- container.style.fontSize = "0.85em";
- }
- container.style.position = "fixed";
- container.style.right = "10px";
- container.style.bottom = "400px";
- container.style.zIndex = "301";
- container.style.overflowY = "scroll";
- container.style.maxHeight = "65vh";
- container.style.maxWidth = "65em";
-
- return container;
- }
-
- function showNotification(text) {
- const newRes = document.getElementById(script_title + "_new_comment");
- newRes.style.display = "";
- newRes.style.color = "#F00";
-
- if (!USE_NOTIFICATION) return;
-
- GM_notification({
- title: "書き込みに新しいレスがあります",
- image: getThreImgSrc(),
- text: text,
- timeout: NOTIFICATION_TIMEOUT,
- onclick: () =>{
- // console.log("notification clicked");
- }
- });
- }
-
- function getResNoFromTdChild(ele) {
- let cno = ele.parentNode.querySelector(".cno");
- let resno = cno.textContent.replace("No.", "");
- return resno;
- }
-
- function checkAkahukuEnabled() {
- return document.getElementById("akahuku_postform") != null
- }
-
- function checkFutakuroEnabled() {
- return document.getElementById("postform") != null;
- }
-
- function expireCommentHistory() {
- const historyList = getListValues();
- // console.log(historyList);
- if (historyList.length > MAX_COMMENT_HISTORY_THREAD) {
- for (let i = 0; i < historyList.length - MAX_COMMENT_HISTORY_THREAD; i++) {
- deleteValue(historyList[i]);
- console.log(script_title + " expire comment history: " + historyList[i]);
- }
- }
- }
-
- function getValue(name) {
- if (!name) return;
- try {
- let val = GM_getValue(name);
- if (!val) return 0;
- // console.log(val);
- return val;
- } catch(e) {
- console.log(e);
- return 0;
- }
- }
-
- function setValue(name, val) {
- if (!name || !val) return;
- try {
- GM_setValue(name, val);
- } catch(e) {
- console.log(e);
- return;
- }
- }
-
- function getListValues() {
- try {
- const gmlistvalues = GM_listValues();
- if (!gmlistvalues) return;
- return gmlistvalues;
- } catch (e) {
- console.log(e);
- return;
- }
- }
-
- function deleteValue(name) {
- if (!name) return;
- try {
- GM_deleteValue(name);
- return;
- } catch (e) {
- console.log(e);
- return 0;
- }
- }
- })();