Skip to content

Instantly share code, notes, and snippets.

@castella-cake
Last active July 1, 2024 11:32
Show Gist options
  • Save castella-cake/6d9955b09d220a439703572308e873f3 to your computer and use it in GitHub Desktop.
Save castella-cake/6d9955b09d220a439703572308e873f3 to your computer and use it in GitHub Desktop.
ニコニコ動画(Re:仮)用のコメントリスト/コメントNGを提供するUserScript。"Raw"をクリックしてインストールできます
// ==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