// ==UserScript== // @name Bangumi Evaluation // @name:zh-CN Bangumi评分脚本・改 // @namespace https://github.com/ipcjs/ // @version 1.1.1 // @description Bangumi Evaluation Script // @description:zh-CN 改造自 http://bangumi.tv/group/topic/345087 // @author ipcjs // @include *://bgm.tv/ep/* // @include *://bgm.tv/character/* // @include *://bgm.tv/blog/* // @include *://bgm.tv/*/topic/* // @include *://bangumi.tv/ep/* // @include *://bangumi.tv/character/* // @include *://bangumi.tv/blogep/* // @include *://bangumi.tv/*/topic/* // @include *://chii.in/ep/* // @include *://chii.in/characterep/* // @include *://chii.in/blog/* // @include *://chii.in/*/topic/* // @compatible chrome // @compatible firefox // @grant none // @run-at document-end // @downloadURL none // ==/UserScript== 'use strict' // type, props, children // type, props, innerHTML // 'text', text const util_ui_element_creator = (type, props, children) => { let elem = null; if (type === "text") { return document.createTextNode(props); } else { elem = document.createElement(type); } for (let n in props) { if (n === "style") { for (let x in props.style) { elem.style[x] = props.style[x]; } } else if (n === "className") { elem.className = props[n]; } else if (n === "event") { for (let x in props.event) { elem.addEventListener(x, props.event[x]); } } else { elem.setAttribute(n, props[n]); } } if (children) { if (typeof children === 'string') { elem.innerHTML = children; } else { for (let i = 0; i < children.length; i++) { if (children[i] != null) elem.appendChild(children[i]); } } } return elem; } const _ = util_ui_element_creator const util_stringify = (item) => { if (typeof item === 'object') { try { return JSON.stringify(item) } catch (e) { console.debug(e) return item.toString() } } else { return item } } const util_ui_alert = function (message, callback, delay) { delay === undefined && (delay = 500) setTimeout(() => { if (callback) { if (window.confirm(message)) { callback() } } else { alert(message) } }, delay) } const addStyle = (css) => { document.head.appendChild(_('style', {}, [_('text', css)])) } const ajax = (...args) => new Promise((resolve, reject) => $.ajax(...args).done(resolve).fail(reject)) // language=CSS addStyle(` .inputButton { background-color: #F09199; color: #fff; cursor: pointer; font-family: lucida grande, tahoma, verdana, arial, sans-serif; font-size: 11px; padding: 1px 3px; text-decoration: none; } .forum_category { background-color: #F09199; color: #fff; font-weight: 700; padding: 3px; } .vote_container { background-color: #e1e7f5 } .forum_boardrow1 { background-color: #fff; border-color: #ebebeb; border-style: solid; border-width: 0; padding: 6px 4px; vertical-align: top } .form-option { float: right; color: #AAA; font-size: 12px; } `) const TRUE = 'Y' const FALSE = '' const HOME_URL_PATH = '/group/topic/345237' const HOME_URL = 'https://bgm.tv' + HOME_URL_PATH const SCORE_REGEX = /^\s*([+-]\d+)(\W[^]*)?$/ // 以数字开头的评论 localStorage.beuj_need_mask === undefined && (localStorage.beuj_need_mask = FALSE) localStorage.beuj_need_suffix === undefined && (localStorage.beuj_need_suffix = TRUE) localStorage.beuj_flag_to_watched === undefined && (localStorage.beuj_flag_to_watched = TRUE) let beuj_only_one_suffix = TRUE // 一个页面最多放一个小尾巴 const COMMENTS_DEFAULT = '力荐 不错 一般 不喜欢 垃圾' let commentTemplates = (localStorage.beuj_comment_templates || COMMENTS_DEFAULT).split(' ') const is_login = !document.querySelector('div.guest') const getUserId = () => { const $avatar = document.querySelector('div.idBadgerNeue a.avatar') return $avatar && $avatar.href.split('/')[4] || '' } const getGh = () => { let $formhash = document.querySelector('#new_comment #ReplyForm > input[name=formhash]') return $formhash.value } const util_page = { ep: () => location.pathname.match(/^\/ep\/\d+$/), group_topic: () => location.pathname.match(/^\/group\/topic\/\d+$/) } const array_last = (arr) => arr[arr.length - 1] const safe_prop = (obj, prop, defaultValue) => obj ? obj[prop] : defaultValue const score_to_index = (score) => 5 - (score + 3) const index_to_score = (index) => 5 - index - 3 const score_to_str = (score) => `${score >= 0 ? '+' : ''}${score}` function readVoteData() { const voteData = { voters: {}, myScore: undefined, myReplyId: undefined, myUserId: getUserId(), hasSuffix: false, clearMyScore: function () { this.myScore = undefined this.myReplyId = undefined delete this.voters[this.myUserId] }, parseReply: function ($reply) { let $message = this.getMessageInReply($reply) let score if ((score = this.getScoreInMessage($message)) !== undefined) { let userId = this.getUserIdInReply($reply) this.voters[userId] = score if (this.myUserId === userId) { this.myScore = score this.myReplyId = $reply.id } if (!this.hasSuffix && $message.innerHTML.includes(HOME_URL_PATH)) { this.hasSuffix = true } return true // 找到了新的评分时, 返回true } return false }, getUserIdInReply: ($reply) => array_last($reply.querySelector(':scope > a.avatar').href.split('/')), getMessageInReply: ($reply) => $reply.querySelector('.message'), getScoreInMessage: function ($message) { if (this._group = $message.innerText.match(SCORE_REGEX)) { let score = Math.min(Math.max(-2, +this._group[1]), 2) return score } return undefined }, getScoreInReply: function ($reply) { return this.getScoreInMessage(this.getMessageInReply($reply)) } } const replys = document.querySelectorAll('.row_reply') for (let $reply of replys) { voteData.parseReply($reply) } console.log('投票数据:', voteData) return voteData } const vote_to_bgm = (score, comment, hasSuffix) => new Promise((resolve, reject) => { // 发送一条推广评论 comment = (comment || '').trim() let text = '' let scoreText = score_to_str(score) text += localStorage.beuj_need_mask ? `[mask]${scoreText}[/mask]` : scoreText comment && (text += ' ' + comment) if (localStorage.beuj_need_suffix && !(beuj_only_one_suffix && hasSuffix)) { (text += `\n[url=${HOME_URL}]--来自Bangumi评分脚本・改[/url]`) } document.querySelector('textarea#content').value = text document.querySelector('#new_comment #ReplyForm [type=submit]').click() resolve('ok') }) function main() { let $comment_list if (!($comment_list = document.getElementById('comment_list'))) { console.log('不存在#comment_list, 不支持投票...') return } // 番剧讨论页: https://bgm.tv/ep/767931 let $container = document.querySelector('#columnEpA .epDesc') // 小组讨论页: https://bgm.tv/group/topic/345237 // 条目讨论版: https://bgm.tv/subject/topic/3022 if (!$container) $container = document.querySelector('div.topic_content') // 人物页: https://bgm.tv/character/77 if (!$container) $container = document.querySelector('#columnCrtB > div.detail') // 日志页面: https://bgm.tv/blog/46986 if (!$container) $container = document.querySelector('#entry_content.blog_entry') // 依然没有, 则创建 if (!$container) { $container = _('div', { className: 'borderNeue', style: { marginTop: '10px' } }) $comment_list.parentElement.insertBefore($container, $comment_list) } const $poll_container = _('div', { id: 'poll_container', style: {/* width: '670px'*/ } }) $container.appendChild($poll_container) let voteData = readVoteData() new MutationObserver((mutations) => { let toRefreshShow = false; // console.log(mutations) for (let mutation of mutations) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { // 当前在评论区删除回复时, 只是display: none, 并不会触发DOM树改变 // 故这里只处理增加了一条回复的情况 if (node.className.split(' ').includes('row_reply')) { toRefreshShow |= voteData.parseReply(node) // 给新增的评论添加 删除/编辑 按钮 let delPath, editPath; const replyIdValue = array_last(node.id.split('_')) if (util_page.ep()) { // 适配ep页面 delPath = `/erase/reply/ep/${replyIdValue}?gh=${getGh()}` editPath = `/subject/ep/edit_reply/${replyIdValue}` } else if (util_page.group_topic()) { // 适配小组讨论页面 delPath = `/erase/group/reply/${replyIdValue}?gh=${getGh()}` editPath = `/group/reply/${replyIdValue}/edit` } if (delPath && editPath) { const onDelClick = (e) => { util_ui_alert('确定删除这条回复?', () => { ajax({ method: 'GET', dataType: 'json', url: `//${location.hostname}${delPath}&ajax=1` }) .then(r => r.status === 'ok' ? r : Promise.reject(r)) .then(r => { node.parentElement.removeChild(node) return r }) .catch(e => { alert('删除失败\n' + util_stringify(e)) }) }, 0) } let $replyInfo = node.querySelector(':scope > .re_info > small') $replyInfo.appendChild(_('text', ' ')) $replyInfo.appendChild(_('a', { href: 'javascript:;', event: { click: onDelClick } }, [_('text', 'del')])) $replyInfo.appendChild(_('text', ' / ')) $replyInfo.appendChild(_('a', { href: editPath }, [_('text', 'edit')])) } } } for (let node of mutation.removedNodes) { // 处理移除reply的情况, 由脚本执行的删除, 会触发移除reply if (node.className.split(' ').includes('row_reply')) { // 移除reply时要做的处理其实比较复杂, 这里做简单化处理, 够用: // 当移除的是自己的包含评分的reply时, 清除自己的评分 if (voteData.getScoreInReply(node) !== undefined && voteData.getUserIdInReply(node) === voteData.myUserId) { voteData.clearMyScore() toRefreshShow = true } } } } } if (toRefreshShow) { show() } }).observe($comment_list, { childList: true, attributes: false, }) show() function show() { if (is_login && voteData.myScore === undefined) { let title = document.title, $tmp // 番剧讨论页: https://bgm.tv/ep/767931 // 人物页: https://bgm.tv/character/77 if ($tmp = document.querySelector('#headerSubject .nameSingle a')) { title = $tmp.innerText if ($tmp = document.querySelector('div#columnEpA h2.title')) { // 番剧讨论页的ep title += ' ' + $tmp.innerText.split(' ')[0] } } const $voteForm = showVote(title, () => { const val = $voteForm.elements.pollOption.value if (!val) { alert("请选择后再投票!"); return; } let score = +val vote_to_bgm(score, $voteForm.elements.comment.value, voteData.hasSuffix) .then((r) => { // 发出评论后, 会触发DOM树改变, 前面的代码监听了DOM树改变, 在必要的时刻会更新投票区域, 故这里不需要手动更新 // voteData.voters[voteData.myUserId] = score // voteData.myScore = score // voteData.myReplyId = safe_prop(array_last(document.querySelectorAll('#comment_list > .row_reply')), 'id', 'no_id') // 评论列表的最后一条 // showVoteResult(voteData) // 在ep页面, 有一个"标记为看过功能" if (util_page.ep() && localStorage.beuj_flag_to_watched) { let epId = array_last(location.pathname.split('/')) return ajax({ method: 'POST', dataType: 'json', url: `//${location.hostname}/subject/ep/${epId}/status/watched?gh=${getGh()}&ajax=1`, }) .then(r => r.status === 'ok' ? r : Promise.reject(r)) .catch(e => { alert(`标记为看过 失败:\n${util_stringify(e)}`) return Promise.reject(e) // 继续抛出异常 }) } else { return 'ok' } }) .then(r => console.log('result:', r)) .catch(e => console.error('error:', e)) }) } else { showVoteResult(voteData) } } function showVoteResult(voteData) { $poll_container.innerHTML = createVoteResultHtml(voteData.voters, voteData.myScore, voteData.myReplyId) } function showVote(title, onSubmit) { $poll_container.innerHTML = createVoteHtml(title) let $voteForm = $poll_container.querySelector('#vote-form') $voteForm.onsubmit = function () { let comment_template let toSubmit = true if (comment_template = $voteForm.elements.comment_template.value) { toSubmit = false let templates = comment_template.split(' ') if (templates.length === commentTemplates.length) { // 更新模板 commentTemplates = templates localStorage.beuj_comment_templates = commentTemplates.join(' ') toSubmit = true } else { alert(`短评模板(${comment_template})不符合格式!\n需要用空格分隔, 例如:\n"${commentTemplates.join(' ')}"`) } } toSubmit && onSubmit() return false // no submit } $voteForm.elements.comment.addEventListener('keydown', (e) => { if (e.ctrlKey && (e.keyCode === 13 || e.keyCode === 10)) { // ctrl + enter $voteForm.elements.voteButton.click() // 直接form.submit()貌似有问题, 只能模拟提交 } }) $voteForm.addEventListener('change', (e) => { let name = e.target.name; let value = e.target.type === 'checkbox' ? (e.target.checked ? TRUE : FALSE) : e.target.value if (name.startsWith('beuj_')) { localStorage[name] = value console.log(name, ' => ', value); } else if (name === 'pollOption') { let score = +value let comment = $voteForm.elements.comment.value // 若简单评论为空, 或是评论模板中的值, 则修改简单评论为评论模板中的一个 if (comment === '' || commentTemplates.includes(comment)) { $voteForm.elements.comment.value = commentTemplates[score_to_index(score)] $voteForm.elements.comment.select() // 全选简评区 } } else if (name === 'modify_comment_template') { $voteForm.elements.comment_template.type = value ? 'text' : 'hidden' } }) $voteForm.elements.beuj_need_mask.checked = localStorage.beuj_need_mask $voteForm.elements.beuj_need_suffix.checked = localStorage.beuj_need_suffix if ($voteForm.elements.beuj_flag_to_watched) { $voteForm.elements.beuj_flag_to_watched.checked = localStorage.beuj_flag_to_watched } return $voteForm } } function createVoteHtml(title) { let rows = '' for (let i = 0; i < commentTemplates.length; i++) { let scoreStr = score_to_str(index_to_score(i)) rows += `
` } return `
${title} 投票
${rows}
${util_page.ep() ? '' : ''}
` } function createVoteResultHtml(voters, myScore, myReplyId) { const counts = new Array(commentTemplates.length).fill(0) // 投+2->-2分的人数的数组 const voterUserIds = Object.keys(voters) for (let userId of voterUserIds) { let score = voters[userId] counts[score_to_index(score)]++ } let voterCount = voterUserIds.length let html = ''; const myIndex = score_to_index(myScore) for (let i = 0; i < commentTemplates.length; i++) { let width = (counts[i] / voterCount * 100).toFixed(1) let isMyVote = myIndex === i html += ` ${score_to_str(index_to_score(i))} ${commentTemplates[i]}${isMyVote ? `(your vote)` : ''}
 
${counts[i]} ${width}% ` } return `
投票结果
${html}
Voters: ${voterCount}
` } main()