Last active
July 1, 2024 11:32
-
-
Save castella-cake/6d9955b09d220a439703572308e873f3 to your computer and use it in GitHub Desktop.
ニコニコ動画(Re:仮)用のコメントリスト/コメントNGを提供するUserScript。"Raw"をクリックしてインストールできます
Raw
Nicokari-CommentList.user.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
// ==UserScript== | |
// @name Nicokari-CommentList | |
// @namespace cyaki_ncrkcl | |
// @match https://www.nicovideo.jp/watch_tmp/* | |
// @grant GM_log | |
// @grant GM_setValue | |
// @grant GM_getValue | |
// @version 0.1.4 | |
// @license CC BY-SA 4.0 | |
// @author CYakigasi | |
// @description ニコニコ動画(Re:仮)用のコメントリスト/コメントNGを提供するUserScript。 | |
// ==/UserScript== | |
(async function() { | |
let storage = JSON.parse(await GM_getValue("settings", "{}")) | |
const NGWords = storage.ngWords ? storage.ngWords : [] | |
// 要素作成 | |
function ce(tagName, obj) { | |
let elem = document.createElement(tagName) | |
if (tagName == "button") { | |
elem.type = "button" | |
} | |
if (obj.placeholder) { | |
elem.placeholder = obj.placeholder | |
} | |
if (obj.textContent) { elem.textContent = obj.textContent } | |
if (obj.style) { elem.style = obj.style } | |
if (obj.id) { elem.id = obj.id } | |
if (obj.type) { elem.type = obj.type } | |
if (obj.attributes) { | |
for ( p in obj.attributes ) { | |
elem.setAttribute(p, obj.attributes[p]) | |
} | |
} | |
if (obj.checked) { | |
elem.checked = obj.checked | |
} | |
return elem | |
} | |
// 主UI | |
function createCommentListUI(comments, blockedCount) { | |
const commentListContainer = ce("div", { style: "position:fixed;right:8px;bottom:0;height:85vh;width:360px;overflow-y:scroll;background:#333;color:#fff;border-radius:4px;padding:0;box-shadow:0px 0px 4px #000a;", id: "ncrk-cl-container" }) | |
const title = ce("div", {textContent: "コメントリスト", style: "padding: 8px;position:sticky;background:#444;top:0;", id: "ncrk-cl-title" }) | |
const stats = ce("div", {textContent: `${comments.length} コメント / ${blockedCount} NG済み / ${comments.length + blockedCount} 受信済み`, style: "font-size: 12px; color:#ddd;", id: "ncrk-cl-stats" }) | |
const toggleAutoScrollLabel = ce("label", { textContent: "自動スクロール", style: "font-size:12px;margin-left:4px;" }) | |
const toggleAutoScroll = ce("input", {type: "checkbox", checked: true }) | |
toggleAutoScrollLabel.appendChild(toggleAutoScroll) | |
const showNgList = ce("button", { textContent: "NGリストを表示", id: "ncrk-cl-button", style: "border-radius: 4px;border: 1px solid #555;background:#ddd;color:#000;padding:0px 4px;font-size:14px;"}) | |
title.appendChild(stats) | |
title.appendChild(showNgList) | |
title.appendChild(toggleAutoScrollLabel) | |
commentListContainer.appendChild(title) | |
document.body.appendChild(commentListContainer) | |
// 行コンテナを準備 | |
const commentRowContainer = ce("div", { style: "", id: "ncrk-cl-commentrowcontainer"}) | |
commentListContainer.appendChild(commentRowContainer) | |
// NGリストコンテナを準備 | |
const ngListContainer = ce("div", { style: "display:none;", id: "ncrk-cl-nglistcontainer"}) | |
commentListContainer.appendChild(ngListContainer) | |
showNgList.addEventListener("click", async function() { | |
if ( showNgList.textContent == "NGリストを表示" ) { | |
showNgList.textContent = "コメントリストに戻る" | |
commentRowContainer.style = "display: none;" | |
ngListContainer.style = "display: block;" | |
} else { | |
showNgList.textContent = "NGリストを表示" | |
commentRowContainer.style = "display: block;" | |
ngListContainer.style = "display: none;" | |
} | |
}) | |
let scrollPosList = {} | |
comments.sort((a,b) => { | |
if ( a.vposMsec > b.vposMsec ) return 1 | |
if ( a.vposMsec < b.vposMsec ) return -1 | |
return 0 | |
}).forEach((elem, index) => { | |
const row = ce("div", { style: "color:#fff;border-top: 1px solid #aaa;padding:4px;display:flex;font-size:14px;text-wrap:nowrap;max-width:360px", id: "ncrk-cl-elem-container" }) | |
const msg = ce("div", { textContent: elem.message, style: "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis", id: "ncrk-cl-elem" }) | |
const time = ce("div", { textContent: `${Math.floor(elem.vposMsec / 1000 / 60)}:${Math.floor(elem.vposMsec / 1000 % 60)} `, style: "color:#ddd", id: "ncrk-cl-elem", attributes: { vposmsec: elem.vposMsec } }) | |
const button = ce("button", { textContent: "NG追加", id: "ncrk-cl-elem-button", style: "border-radius: 4px;border: 1px solid #555;background:#ddd;color:#000;padding:0px 4px;font-size:14px;margin-left:4px;"}) | |
button.addEventListener("click", async function() { | |
alert("NGに追加しました。リロードします...") | |
let latestStorage = JSON.parse(await GM_getValue("settings", "{}")) | |
if ( latestStorage.ngWords ) { | |
latestStorage.ngWords = [...latestStorage.ngWords, elem.message] | |
} else { | |
latestStorage.ngWords = [elem.message] | |
} | |
GM_setValue("settings", JSON.stringify(latestStorage)) | |
location.reload() | |
}) | |
row.appendChild(msg) | |
row.appendChild(time) | |
row.appendChild(button) | |
commentRowContainer.appendChild(row) | |
//time.textContent = `${time.textContent} ${row.offsetHeight * (index + 1) + title.offsetHeight}` | |
// 初期状態で見えないのであれば、スクロールするリストに入れておく | |
if ( row.offsetHeight * (index + 1) + title.offsetHeight > document.documentElement.clientHeight ) scrollPosList[`${Math.floor( elem.vposMsec / 1000 )}`] = row | |
}) | |
//GM_log(scrollPosList) | |
// 追従 | |
const timeCounterObserver = new MutationObserver(records => { | |
const minSecArray = records[0].addedNodes[0].textContent.split(":") | |
const sec = Number(minSecArray[0]) * 60 + Number(minSecArray[1]) | |
//GM_log(``) | |
if ( scrollPosList[`${sec}`] && toggleAutoScroll.checked ) { | |
// スクロールするべき要素があるならスクロールする | |
scrollPosList[`${sec}`].scrollIntoView({ behavior: "smooth", block: "end" }) | |
//commentListContainer.scrollTo({ top: scrollPosList[`${sec}`], behavior: "smooth" }) | |
} | |
}) | |
// まずmainをobserveする | |
const mainElem = document.querySelector("main") | |
const mainObserver = new MutationObserver(records => { | |
const timeCounterElem = document.querySelector("main > div > .pos_relative > .d_flex > .fs_12.font_alnum > span:first-child") // 愚行 | |
if ( timeCounterElem ) { | |
timeCounterObserver.observe(timeCounterElem, { | |
childList: true, | |
characterData: true, | |
}) | |
// 見つかったらobserveしてこのobserverは破壊 | |
mainObserver.disconnect() | |
} | |
}) | |
mainObserver.observe(mainElem, { | |
childList: true, | |
subtree: true, | |
}) | |
// NGリスト構築 | |
const ngAddInput = ce("input", { style: "color:#fff;border: 1px solid #aaa;padding:4px;font-size:14px;background:#222;", placeholder: "NG追加するワードを入力(部分一致)", type: "text" }) | |
const ngAddButton = ce("button", { textContent: "追加", id: "ncrk-cl-elem-button", style: "border-radius: 4px;border: 1px solid #555;background:#ddd;color:#000;padding:0px 4px;font-size:14px;margin-left:4px;"}) | |
ngListContainer.appendChild(ngAddInput) | |
ngListContainer.appendChild(ngAddButton) | |
ngAddButton.addEventListener("click", async function() { | |
if (ngAddInput.value != "") { | |
alert("NGに追加しました。リロードします...") | |
let latestStorage = JSON.parse(await GM_getValue("settings", "{}")) | |
if ( latestStorage.ngWords ) { | |
latestStorage.ngWords = [...latestStorage.ngWords, ngAddInput.value] | |
} else { | |
latestStorage.ngWords = [ngAddInput.value] | |
} | |
GM_setValue("settings", JSON.stringify(latestStorage)) | |
location.reload() | |
} | |
}) | |
NGWords.forEach(elem => { | |
const row = ce("div", { style: "color:#fff;border-top: 1px solid #aaa;padding:4px;display:flex;font-size:14px;", id: "ncrk-cl-elem-container" }) | |
const text = ce("div", { textContent: elem, style: "flex:1;", id: "ncrk-cl-elem" }) | |
const button = ce("button", { textContent: "削除", id: "ncrk-cl-elem-button", style: "border-radius: 4px;border: 1px solid #555;background:#ddd;color:#000;padding:0px 4px;font-size:14px;margin-left:4px;"}) | |
button.addEventListener("click", async function() { | |
const confirm = window.confirm("NGが変更されました。変更はリロードするまで適用されません。リロードしますか?") | |
let latestStorage = JSON.parse(await GM_getValue("settings", "{}")) | |
if ( latestStorage.ngWords ) { | |
latestStorage.ngWords = latestStorage.ngWords.filter(word => { return word != elem }) | |
} | |
GM_setValue("settings", JSON.stringify(latestStorage)) | |
if ( confirm ) { | |
location.reload() | |
} else { | |
row.remove() | |
} | |
}) | |
row.appendChild(text) | |
row.appendChild(button) | |
ngListContainer.appendChild(row) | |
}) | |
} | |
const originalFetch = unsafeWindow.fetch | |
unsafeWindow.fetch = async function fetch(url, param) { | |
const result = await originalFetch(url, param) | |
//GM_log(result) | |
if ( param.method != "POST" && result.url.startsWith("https://nvapi.nicovideo.jp/v1/tmp/comments/") ) { | |
// route-DLaBnqUK.js が PlayTime-Byupm8uv.js がフェッチしたコメントデータ を p としてインポートして、 p.json() で呼ぶっぽいので、json関数だけオーバーライドします | |
return {...result, json: function() { | |
// 一応asyncとして作る。 | |
return new Promise(async (resolve, reject) => { | |
// とりあえず元のjsonを取得して、200かどうか確認する | |
let commentData = await result.json() | |
if ( commentData.meta.status == 200 ) { | |
let blockedCount = 0 | |
// 配列をfilterして、NGWordsに該当するかチェック。 | |
commentData.data.comments = commentData.data.comments.filter(elem => { | |
for ( word of NGWords ) { | |
// 含むならfalseしてここで帰れ | |
if (elem.message.includes(word)) { | |
blockedCount++ | |
return false | |
} | |
} | |
// 生還したらtrue | |
return true | |
}) | |
createCommentListUI(commentData.data.comments, blockedCount) | |
resolve(commentData) | |
} else { | |
// 200じゃなかったらもう知らん | |
resolve(commentData) | |
} | |
//GM_log(commentData) | |
resolve(commentData) | |
}) | |
}} | |
} else { | |
return result | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment