// ==UserScript== // @name NGA检查帖子可见状态 // @namespace https://github.com/stone5265/GreasyFork-NGA-Check-Post-Status // @version 1.0.2 // @author stone5265 // @description 不可见楼层提醒 与 可见状态关注列表 // @license MIT // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/localforage/1.10.0/localforage.min.js#sha512=+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng== // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @exclude */nuke.php* // @exclude */misc/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @grant unsafeWindow // @inject-into content // @downloadURL https://update.greasyfork.icu/scripts/531676/NGA%E6%A3%80%E6%9F%A5%E5%B8%96%E5%AD%90%E5%8F%AF%E8%A7%81%E7%8A%B6%E6%80%81.user.js // @updateURL https://update.greasyfork.icu/scripts/531676/NGA%E6%A3%80%E6%9F%A5%E5%B8%96%E5%AD%90%E5%8F%AF%E8%A7%81%E7%8A%B6%E6%80%81.meta.js // ==/UserScript== (function () { const WATCHLIST_CHECK_INTERVAL = 1000 const POSTBOX_CHECK_INTERVAL = 500 'use strict' const CheckPostStatus = { name: 'CheckPostStatus', title: 'NGA检查帖子可见状态', desc: '检查自己发布的 主题/回复 别人是否能看见', settings: [ { type: 'advanced', key: 'autoCheckInterval', title: '自动检查的间隔 (分钟)', desc: '自动检查关注列表的间隔,当处于某个版面的第一页时触发\n(最短间隔为5分钟,建议不少于30分钟)', default: 60, min: 5, menu: 'left' }, { type: 'advanced', key: 'expireDays', title: '关注过期的天数', desc: '关注过期的天数,过期的关注在“检查全部”时不会进行检查\n(-1为永不过期)', default: 120, min: -1, menu: 'left' }, { type: 'advanced', key: 'autoDeleteAfterDays', title: '关注过期后自动删除的天数', desc: '关注过期的天数,过期的关注在“检查全部”时不会进行检查\n(-1为不进行自动删除)', default: 1, min: -1, menu: 'left' }, { type: 'advanced', key: 'isAutoCheck', title: '自动检查开关', desc: '开启后会定期对“关注列表”中未过期的关注进行自动检查', default: false, menu: 'right' }, { type: 'advanced', key: 'isWatchButton', title: '关注按钮开关', desc: '开启后会在“点赞”按钮旁边添加“关注”按钮,用于将该楼层添加进“关注列表”', default: true, menu: 'right' }, { type: 'advanced', key: 'isFidWarning', title: '版面提示开关', desc: '开启后会对受限版面提示“该版面需要登陆才能访问,不支持[关注按钮]”', default: true, menu: 'right' } ], store: null, cacheFid: {}, lastVisibleCheckUrl: '', lastMissingCheckUrl: '', visibleFloors: new Set(), lock: new Promise(() => {}), locks: new Array(20).fill(new Promise(() => {})), initFunc() { const this_ = this // 创建储存实例 this.store = script.createStorageInstance('NGA_BBS_Script__CheckPostStatus') // 初始化的时候清除超过一定天数的过期关注 const currentTime = Math.floor(Date.now() / 1000) // 秒 let removedCount = 0 this.store.iterate((record, key) => { const isPermanent = record.expireTime === -1 if (!isPermanent && currentTime >= record.expireTime) { const expireDays = Math.floor((record.expireTime - currentTime) / 60 / 60 / 12) const isAutoDelete = script.setting.advanced.autoDeleteAfterDays >= 0 if (isAutoDelete && expireDays >= script.setting.advanced.autoDeleteAfterDays) { this_.store.removeItem(key) removedCount += 1 } } }) .then(() => { script.printLog(`${this.title}: 已清除${removedCount}条过期关注`) }) .catch(err => { console.error(`${this.title}清除超期数据失败,错误原因:`, err) }) document.body.addEventListener('click', function(e) { if (!e.target) return let el // 点击"关注该楼层可见状态"按钮 el = e.target.closest('.cps__watch-icon') if (el) { const clickedButton = el const anotherButton = Array.from(clickedButton.parentElement.querySelectorAll('.cps__watch-icon')).filter(x => x !== clickedButton)[0] // 切换显示状态 clickedButton.style.display = 'none' anotherButton.style.display = '' const type = clickedButton.getAttribute('data-type') const href = clickedButton.getAttribute('data-href') const floorNum = clickedButton.getAttribute('data-floor') const params = this_.getUrlParams(href) const key = `tid=${params['tid']}&pid=${params['pid']}` if (type === 'unwatch') { // 添加关注 const isPermanent = script.setting.advanced.expireDays < 0 const expireTime = isPermanent ? -1 : Math.floor(Date.now() / 1000) + script.setting.advanced.expireDays * 24 * 60 * 60 // 秒 this_.store.setItem(key, { topicName: document.title.replace(/\sNGA玩家社区/g, ''), floorNum: parseInt(floorNum), isVisible: null, checkTime: null, expireTime: expireTime }) .then(() => { this_.reloadWatchlist() }) } else { // 取消关注 this_.store.removeItem(key) .then(() => { this_.reloadWatchlist() }) } return } // 点击"重置时间"或者"永久关注"按钮 el = e.target.closest('.cps__wl-change-expire-time') if (el) { const key = el.getAttribute('data-key') const time = el.getAttribute('data-time') const expireTime = time === '-1' ? -1 : Math.floor(Date.now() / 1000) + script.setting.advanced.expireDays * 24 * 60 * 60 // 秒 this_.store.getItem(key) .then(record => { this_.store.setItem(key, { ...record, expireTime: expireTime }) .then(() => { this_.reloadWatchlist() }) }) return } // 点击"检查"按钮 el = e.target.closest('.cps__wl-check') if (el) { const key = el.getAttribute('data-key') this_.checkRowVisible(key) .then(isVisible => { script.popMsg(`检查完成,目标位于${isVisible ? '可见' : '不可见'}状态`) this_.reloadWatchlist() }) return } // 点击"删除"按钮 el = e.target.closest('.cps__wl-del') if (el) { const key = el.getAttribute('data-key') this_.store.removeItem(key) .then(() => { this_.reloadWatchlist() }) return } // 点击"刷新"按钮 el = e.target.closest('.cps__panel-refresh') if (el) { this_.reloadWatchlist() return } // 点击"检查全部"按钮 el = e.target.closest('.cps__panel-checkall') if (el) { const execute = async (el) => { el.textContent = '检查中...' el.disabled = true el.style.opacity = 0.6 el.style.cursor = 'not-allowed' try { const rows = await this_.getSurvivalRows() let invisibleNum = 0 let processed = 0 for (const key of rows) { const isVisible = await this_.checkRowVisible(key) if (!isVisible) { ++invisibleNum } ++processed this_.reloadWatchlist() el.textContent = `检查中... (${processed}/${rows.length})` if (processed < rows.length) { // 设置检查间隔 await new Promise(resolve => setTimeout(resolve, WATCHLIST_CHECK_INTERVAL)) } } if (rows.length !== 0) { script.popMsg(`检查完成,总共检查了${rows.length}个楼层,其中${invisibleNum}个位于不可见状态`) } } catch (err) { script.popMsg(`失败!${err.message}`, 'err') } finally { el.textContent = '检查所有' el.disabled = false el.style.opacity = '' el.style.cursor = '' } } execute(el) return } // 点击"清除过期关注"按钮 el = e.target.closest('.cps__panel-clean-expired') if (el) { this_.cleanExpiredData() .then(() => { this_.reloadWatchlist() }) return } // 点击"清空*所有*关注"按钮 el = e.target.closest('.cps__panel-clean-all') if (el) { this_.cleanLocalData() .then(() => { this_.reloadWatchlist() }) return } // 切换选项卡 el = e.target.closest('.cps__tab-header > span') if (el) { const _index = [...el.parentNode.children].indexOf(el) document.querySelectorAll('.cps__tab-header > span').forEach((el, index) => { if (index == _index) { el.classList.add('cps__tab-active') } else { el.classList.remove('cps__tab-active') } }) document.querySelectorAll('.cps__tab-content').forEach((el, index) => { if (index == _index) { el.classList.add('cps__tab-active') } else { el.classList.remove('cps__tab-active') } }) this_.reloadWatchlist() return } // 关闭"关注列表" el = e.target.closest('.cps__list-panel .cps__panel-close') if (el) { if (el.getAttribute('data-close-type') === 'hide') { el.parentElement.style.display = 'none' } else { el.parentElement.remove() } return } // 点击"设置面板"按钮 el = e.target.closest('.cps__panel-settings') if (el) { const cover = document.getElementById('cps__setting-cover') const panel = document.getElementById('cps__setting-panel') cover.style.display = 'block' cover.style.position = 'fixed' panel.style.display = 'block' return } }) // 关注列表 GM_registerMenuCommand('关注列表', function () { if (document.getElementById('cps__watchlist-panel') !== null) return document.body.insertAdjacentHTML('beforeend', `
×

关注列表(全部)关注列表(不可见)
主题 楼层 状态 上次检查 剩余时间 操作
主题 楼层 状态 上次检查 剩余时间 操作
`) this_.reloadAutoCheckInfo() this_.reloadWatchlist() }) }, // 位于帖子列表页时自动检查关注列表 async renderThreadsFunc(topicrow) { // async renderThreadsFunc($el) { // 未勾选自动检查开关时, 不进行自动检查 if (!script.setting.advanced.isAutoCheck) return // 位于列表页第一页的第一个帖子时才触发自动检查 if (topicrow.querySelector('a').getAttribute('id') !== 't_rc1_0') return // const $ = script.libs.$ const lastAutoCheckTime = await script.getValue('cps__lastAutoCheckTime') const currentTime = Math.floor(Date.now() / 1000 / 60) // 分钟 const deltaTime = currentTime - parseFloat(lastAutoCheckTime) const autoCheckInterval = script.setting.advanced.autoCheckInterval // 距离上次自动检查小于设置的间隔时, 不进行自动检查 if (lastAutoCheckTime && deltaTime < autoCheckInterval) return try { const rows = await this.getSurvivalRows() // 关注列表为空时, 不进行自动检查 if (rows.length === 0) return script.popMsg(`[自动检查]开始...预计检查${rows.length}个关注`) let invisibleNum = 0 let processed = 0 for (const key of rows) { const isVisible = await this.checkRowVisible(key) if (!isVisible) { ++invisibleNum } ++processed this.reloadWatchlist() document.querySelectorAll('.cps__panel-checkall').forEach(button => { button.textContent = `检查中... (${processed}/${rows.length})` button.disabled = true }) if (processed < rows.length) { await new Promise(resolve => setTimeout(resolve, WATCHLIST_CHECK_INTERVAL)) } } script.popMsg(`[自动检查]总共检查了${rows.length}个关注,其中${invisibleNum}个位于不可见状态`) // 更新最后一次自动检查时间 script.setValue('cps__lastAutoCheckTime', String(currentTime)) } catch (err) { script.popMsg(`[自动检查]失败!${err.message}`, 'err') // 更新最后一次自动检查时间 script.setValue('cps__lastAutoCheckTime', String(currentTime)) } finally { document.querySelectorAll('.cps__panel-checkall').forEach((button, index) => { button.textContent = '检查所有' if (index == 0) { button.disabled = false } }) this.reloadAutoCheckInfo() } }, async renderFormsFunc(postbox) { const this_ = this // 去除 #后的内容 比如 (/read.php?tid=xxx&page=1#pidxxxxAnchor 去掉#pidxxxxAnchor) const checkUrl = document.baseURI.split('#')[0] // 检查检查详细页缺失的楼层 (目前账号无法看到的楼层) this.checkMissingFloors(checkUrl) /** * "tid={}(&authorid={})(&page={})" */ const queryString = checkUrl.split('?')[1] const els = postbox.querySelectorAll('td.c2 a') /** * "pid{}Anchor" */ const pidAnchor = els[0].getAttribute('id') /** * "l{}" */ const floorName = els[1].getAttribute('name') const currentFloor = parseInt(floorName.slice(1)) /** * "/read.php?tid={}(&authorid={})&page={}#pid{}Anchor" */ const href = `/read.php?${queryString}${queryString.includes('&page=') ? '' : '&page=1'}#${pidAnchor}` const params = this.getUrlParams(href) // 检查该版面是否需要登录才能查看 let isLimit try { isLimit = await this.checkFidLimit(__CURRENT_FID) } catch (error) { isLimit = true console.log(error) } // 若该版面需要登录才能访问, 则不支持部分功能 if (isLimit) { // 当前版面只提示一次 const lastWarningFid = await script.getValue('cps__lastWarningFid') if (__CURRENT_FID !== lastWarningFid) { script.setValue('cps__lastWarningFid', __CURRENT_FID) if (script.setting.advanced.isFidWarning) { script.popMsg('该版面需要登陆才能访问,不支持[关注按钮]', 'warn') } } } // 添加"关注该楼层可见状态"按钮 if (script.setting.advanced.isWatchButton && !isLimit) { const key = `tid=${params['tid']}&pid=${params['pid']}` const watching = await this.store.getItem(key) !== null postbox.querySelector('.small_colored_text_btn.block_txt_c2.stxt').insertAdjacentHTML('beforeend', ` 🔵`) } // 检查该页面下登录用户的发言 const uid = parseInt(postbox.querySelector('a[name="uid"]').textContent) if (!isNaN(__CURRENT_UID) && uid === __CURRENT_UID) { const tagBlock = postbox.querySelector('.small_colored_text_btn.block_txt_c2.stxt') tagBlock.insertAdjacentHTML('beforeend', ' 检测中... ') if (!isLimit) { // (正常区) 使用游客状态对当前页可见楼层进行标记 if (checkUrl !== this.lastVisibleCheckUrl) { this.lastVisibleCheckUrl = checkUrl // 记录当前页游客可见楼层号 this.visibleFloors = new Set() const execute = debounce(async () => { const result = this_.requestWithoutAuth(checkUrl) .then(({ success, html }) => { if (success) { // 记录当前页面所有游客能看到的楼层号 for (const floor of html.querySelectorAll('td.c2')) { const visibleFloor = parseInt(floor.querySelectorAll('a')[1].getAttribute('name').slice(1)) this_.visibleFloors.add(visibleFloor) } } }) return result }, 1500) this.lock = execute() } await this.lock } else { // (需要登录才能进的区) 单独向每个属于登录用户的楼层发送一条编辑请求 if (checkUrl !== this.lastVisibleCheckUrl) { this.lastVisibleCheckUrl = checkUrl this.visibleFloors = new Set() this.locks = Array(20).fill().map(() => { let resolveFn const promise = new Promise(resolve => resolveFn = resolve) return { promise, resolveFn } }) const floors = Object.keys(commonui.postArg.data) for (let floor of floors) { if (isNaN(floor)) continue floor = parseInt(floor) // 如果处理完已经切换到其他页面, 则放弃对该页的后续操作 if (!(floor in commonui.postArg.data)) { this.locks.forEach(lock => lock.resolveFn()) return } const data = commonui.postArg.data[floor] if (parseInt(data.pAid, 10) !== __CURRENT_UID) continue const { success } = await new Promise((resolve) => { fetch(`/post.php?lite=js&action=modify&tid=${data.tid}&pid=${data.pid}`) .then((res) => res.blob()) .then((blob) => { const reader = new FileReader() reader.onload = () => { const text = reader.result const result = JSON.parse( text.replace("window.script_muti_get_var_store=", "") ) const { data, error } = result if (error) { resolve({ success: false }) return } if (data && data['post_type'] & 2) { // resolve('只有作者/版主可见') resolve({ success: false }) return } fetch(`/post.php?lite=js&tid=${data.tid}&pid=${data.pid}`) resolve({ success: true }) } reader.readAsText(blob, "GBK") }) .catch(() => { resolve({ success: false }) }) }) if (success) { this.visibleFloors.add(floor) } this.locks[floor % 20].resolveFn() await new Promise(resolve => setTimeout(resolve, POSTBOX_CHECK_INTERVAL)) } } await this.locks[currentFloor % 20].promise } const isVisible = this.visibleFloors.has(currentFloor) // 如果楼层切换的比较快,等这页的游客访问完早已切换到另一页,则放弃对该楼的后续操作 if (!document.contains(postbox)) { return } // 对不可见的楼层添加标记并提示 let tag if (!isVisible) { const floorName = currentFloor === 0 ? '主楼' : `${currentFloor}楼` tag = ' [不可见] ' script.popNotification(`当前页检测到${floorName}其他人不可见`, 4000) } else { tag = ' 可见 ' } tagBlock.querySelector('.visibility_text').remove() tagBlock.insertAdjacentHTML('beforeend', tag) } }, /** * 游客状态访问 * @method requestWithoutAuth * @param {string} url */ requestWithoutAuth(url) { // const $ = script.libs.$ return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, anonymous: true, // enforces 'fetch' mode overrideMimeType: "text/html; charset=utf-8", responseType: 'ArrayBuffer', // fetch模式下该设置无效 onload: function(response) { const text = response.response instanceof ArrayBuffer ? new TextDecoder('gbk').decode(response.response) : response.response if (response.status === 200) { const parser = new DOMParser() resolve({ success: true, html: parser.parseFromString(text, 'text/html') // $html: $(text) }) } // 获取错误信息 const errorCode = text.match(/(ERROR:([\d]+))/)[2] const errorMessage = `(ERROR:${errorCode})` // let errorCode // let errorMessage // if (response.response instanceof ArrayBuffer) { // errorCode = text.match(/(ERROR:([\d]+))/)[2] // errorMessage = text.match(/([^<]+)<\/title>/)[1] // } else { // errorCode = text.match(/(ERROR:<!--msgcodestart-->([\d]+)<!--msgcodeend-->)/)[2] // errorMessage = `(ERROR:${errorCode})` // } // "(ERROR:15)访客不能直接访问" 进行跳转后可访问 if (errorCode === '15') { // 跳转所需要用到的游客cookie const lastvisit = response.responseHeaders.match(/lastvisit=([^;]+)/)[0] const ngaPassportUid = response.responseHeaders.match(/ngaPassportUid=([^;]+)/)[0] const guestJs = text.match(/guestJs=([^;]+)/)[0] // 添加随机参数防止缓存 const r = Math.floor(Math.random()*1000) const finalUrl = response.finalUrl.replace(/(?:\?|&)rand=\d+/,'')+'&rand=' + r // 携带游客cookie后再次访问 GM_xmlhttpRequest({ method: 'GET', url: finalUrl, headers: { "Cookie": `${lastvisit}; lastpath=0; ${ngaPassportUid}; ${guestJs}`, 'Referer': response.finalUrl }, anonymous: true, onload: function(response) { if (response.status === 200) { const parser = new DOMParser() resolve({ success: true, html: parser.parseFromString(response.responseText, 'text/html') }) // resolve({ // success: true, // $html: $(response.responseText) // }) } else { const errorCode = text.match(/(ERROR:<!--msgcodestart-->([\d]+)<!--msgcodeend-->)/)[2] console.error(`(ERROR:${errorCode})`) resolve({ success: false }) } }, onerror: function(error) { console.error(error) resolve({ success: false }) } }) } else { console.error(errorMessage) resolve({ success: false }) } }, onerror: function(error) { console.error(error) resolve({ success: false }) } }) }) }, /** * 检查该页面缺失的楼层 (目前账号无法看到的楼层) * @method checkMissingFloors * @param {string} checkUrl */ checkMissingFloors(checkUrl) { // const $ = script.libs.$ if (checkUrl === this.lastMissingCheckUrl) return if (!commonui.postArg || !commonui.postArg.def) return // 等待加载完页面 this.lastMissingCheckUrl = checkUrl // 若是只查看某个回复,则不必检查 if (checkUrl.match(/pid=([\d]+)/) !== null) return // 倒序模式 const isReversed = commonui.postArg.def.tmBit1 & 262144 // 只看作者模式 const isOnlyAuthor = checkUrl.match(/authorid=/) !== null // 该贴总回帖数 const maxFloor = commonui.postArg.def.tReplies // 获取当前所在页的页数 (注: 使用 __PAGE[2] 获取的当前页数 在点击"加载下一页"按钮时 获取的还是当前页而非新加载出来的一页的页数) const pageMatch = checkUrl.match(/page=([\d]+)/) const __PAGE = commonui.postArg.w.__PAGE || [] // 正序模式回帖或者编辑, 前者page=e, 后者不会出现page= const currentPage = pageMatch ? parseInt(pageMatch[1]) : (__PAGE && __PAGE[2] ? __PAGE[2] : 1) // 是否为最后一页 const isLastPage = pageMatch === null || currentPage === (__PAGE && __PAGE[1]) // 该页开始楼层号 let startFloor // 该页截止楼层号 let endFloor // 记录当前页目前账号能看到的楼层 const currPageFloors = new Set() document.querySelectorAll('.forumbox .postrow').forEach(el => { const floor = parseInt(el.getAttribute('id').split('strow')[1]) currPageFloors.add(floor) }) if (isOnlyAuthor) { // 不支持倒序模式下的只看作者 if (isReversed) { script.popMsg('[检查缺失楼层]不支持倒序模式下的只看作者', 'warn') return } // 只看作者模式的最后一页只能使用该页能看到的楼层中最大楼层号 if (isLastPage) { startFloor = Math.max(1, (currentPage - 1) * 20) endFloor = Math.max(...currPageFloors) } } else { if (!isReversed) { // 正序模式通过该页页数来计算范围 (并对其进行阻断来保证最后一页范围计算正确) startFloor = Math.max(1, (currentPage - 1) * 20) endFloor = Math.min(maxFloor, currentPage * 20 - 1) } else { // 倒序模式通过模拟来计算当前页楼层号的范围 // 第一页跳过主楼 let iPage = 1 endFloor = maxFloor startFloor = endFloor - 18 // 第二页到当前页 ++iPage while (iPage <= currentPage) { endFloor -= 20 startFloor -= 20 ++iPage } // 截断最后一页的开始楼号 startFloor = Math.max(1, startFloor) } } // 主楼检查 (用于只看作者模式) if (currentPage === 1 && !currPageFloors.has(0)) { script.popNotification(`当前页检测到0楼缺失`, 4000) } let count = 0 if (!isReversed) { // 正序提示 for (let i = Math.max(1, startFloor); i <= Math.min(maxFloor, endFloor); ++i) { // 一页最多存在20个楼层, 超出则说明代码有BUG, 终止提示防止不必要的开销 if (++count > 20) break if (!currPageFloors.has(i)) { script.popNotification(`当前页检测到${i}楼缺失`, 4000) } } } else { // 倒序提示 for (let i = Math.min(maxFloor, endFloor); i >= Math.max(1, startFloor); --i) { // 一页最多存在20个楼层, 超出则说明代码有BUG, 终止提示防止不必要的开销 if (++count > 20) break if (!currPageFloors.has(i)) { script.popNotification(`当前页检测到${i}楼缺失`, 4000) } } } }, /** * 检查该版面是否需要登录才能查看 * @method checkFidLimit * @param {number} fid */ async checkFidLimit(fid) { // 对版面限制进行缓存 if (this.cacheFid[fid] === undefined) { this.cacheFid[fid] = this.requestWithoutAuth(`/thread.php?fid=${fid}`) .then(({ success }) => { return !success }) .catch(error => { console.error(`checkFidLimit(fid=${fid}): `, error) return true }) } return this.cacheFid[fid] }, /** * 获取URL参数对象 * @method getUrlParams * @param {string} url"/read.php?tid={}(&authorid={})&page={}#pid{}Anchor" * @return {Object} 参数对象 */ getUrlParams(url) { let params = {} const $ = url.split('#') const url_ = $[0] const pid = parseInt($[1].slice(3, -6)) const queryString = url_.split('?')[1] queryString.split('&').forEach(item => { const $ = item.split('=') if ($[0] && $[1]) { params[$[0]] = parseInt($[1]) } }) params['pid'] = pid return params }, /** * 重新渲染自动检查信息 * @method reloadWatchlist */ async reloadAutoCheckInfo() { if(!document.getElementById('cps__watchlist-panel')) return const autoCheckInfo = document.getElementById('cps__auto-check-info') const isAutoCheck = script.setting.advanced.isAutoCheck const interval = `${script.setting.advanced.autoCheckInterval}分钟` const lastAutoCheckTime = await script.getValue('cps__lastAutoCheckTime') let lastCheck if (lastAutoCheckTime) { const timestamp = new Date(parseInt(lastAutoCheckTime) * 60 * 1000) lastCheck = timestamp.toLocaleString().slice(0, -3) } else { lastCheck = '-' } autoCheckInfo.replaceChildren() autoCheckInfo.insertAdjacentHTML('beforeend', `自动检查: ${isAutoCheck ? `<span style="color: green;">on</span> | 检查间隔: ${interval} | 上次自动检查时间: ${lastCheck}` : '<span style="color: grey;">off</span>'}`) }, /** * 重新渲染关注列表 * @method reloadWatchlist */ reloadWatchlist() { if(!document.getElementById('cps__watchlist-panel')) return let isWatchlistInbisible let watchlist = document.querySelector('.cps__tab-active #cps__watchlist') if (watchlist) { isWatchlistInbisible = false } else { watchlist = document.querySelector('.cps__tab-active #cps__watchlist-invisible') if (!watchlist) return isWatchlistInbisible = true } let expiredRows = [] let rows = [] this.store.iterate((record, key) => { if (isWatchlistInbisible && record.isVisible !== false) return const currentTime = Math.floor(Date.now() / 1000) // 秒 const isPermanent = record.expireTime === -1 const isSurvival = isPermanent || currentTime < record.expireTime let timeLeft if (isSurvival) { if (isPermanent) { timeLeft = '永久' } else { timeLeft = Math.floor((record.expireTime - currentTime) / 60 / 60) if (timeLeft === 0) { timeLeft = '<1小时' } else if (timeLeft < 24) { timeLeft = `${timeLeft}小时` } else { timeLeft = `${Math.floor(timeLeft / 24)}天` } } } else { timeLeft = Math.floor((currentTime - record.expireTime) / 60 / 60) if (timeLeft === 0) { timeLeft = '已过期(<1小时)' } else if (timeLeft < 24) { timeLeft = `已过期(${timeLeft}小时)` } else { timeLeft = `已过期(${Math.floor(timeLeft / 24)}天)` } } let timeSinceLastCheck let visibleStatus if (record.checkTime !== null) { timeSinceLastCheck = Math.floor((currentTime - record.checkTime) / 60) if (timeSinceLastCheck === 0) { timeSinceLastCheck = '<1分钟' } else if (timeSinceLastCheck < 60 * 3) { timeSinceLastCheck = `${timeSinceLastCheck}分钟前` } else if (timeSinceLastCheck < 60 * 24) { timeSinceLastCheck = `${Math.floor(timeLeft / 60)}小时前` } else { timeSinceLastCheck = '超过1天' } visibleStatus = record.isVisible ? '可见' : '<span style="color: red; font-weight: bold;">不可见</span>' } else { timeSinceLastCheck = '-' visibleStatus = '-' } const floor = record.floorNum === 0 ? '主楼' : `${record.floorNum}楼` const keywords = key.split('&') // key='tid={}&pid={}' const query = keywords[1] === 'pid=0' ? keywords[0] : keywords[1] // 对应楼层跳转链接 const href = `/read.php?${query}&opt=128` const context = ` <tr> <td style="text-align:left;" title="${record.topicName}">${record.topicName}</td> <td title="${href}"><a href="${href}" class="urlincontent">${floor}</a></td> <td>${visibleStatus}</td> <td title="${timeSinceLastCheck}">${timeSinceLastCheck}</td> <td title="${timeLeft}">${timeLeft}</td> <td> <button class="cps__wl-change-expire-time cps__help" help="重置剩余时间为设置的关注过期天数" data-key="${key}" data-time="reset" >重置</span> <button class="cps__wl-change-expire-time cps__help" help="将剩余时间设置为永不过期" data-key="${key}" data-time="-1" help="将剩余时间设置为永不过期">永久</span> <button class="cps__wl-check" data-key="${key}">检查</span> <button class="cps__wl-del" data-key="${key}">删除</span> </td> </tr> ` if (isSurvival) { rows.push([key, context]) } else { expiredRows.push([key, context]) } }) .then(() => { watchlist.replaceChildren() // 按照tid进行排序 expiredRows.sort((a, b) => a[0].localeCompare(b[0])) rows.sort((a, b) => a[0].localeCompare(b[0])) // 将过期关注放在最上面 expiredRows.forEach(row => watchlist.insertAdjacentHTML('beforeend', row[1])) rows.forEach((row) => watchlist.insertAdjacentHTML('beforeend', row[1])) }) }, /** * 获取关注列表中所有未失效的行 * @method getSurvivalRows */ async getSurvivalRows() { const currentTime = Math.floor(Date.now() / 1000) // 秒 const rows = [] await this.store.iterate((record, key) => { const isPermanent = record.expireTime === -1 const isSurvival = isPermanent || currentTime < record.expireTime if (isSurvival) { rows.push(key) } }) return rows }, /** * 检查关注列表中某一行的可见状态 * @method checkRowVisible */ async checkRowVisible(key) { const keywords = key.split('&') // key='tid={}&pid={}' const query = keywords[1] === 'pid=0' ? keywords[0] : keywords[1] const href = `/read.php?${query}` const { success, html } = await this.requestWithoutAuth(href) const isVisible = success && html.querySelector('table.forumbox.postbox') !== null const record = await this.store.getItem(key) await this.store.setItem(key, { ...record, isVisible: isVisible, checkTime: Math.floor(Date.now() / 1000) // 秒 }) return isVisible }, /** * 清除过期关注 * @method cleanLocalData */ async cleanExpiredData() { this.store.iterate((record, key) => { const currentTime = Math.floor(Date.now() / 1000) // 秒 const isPermanent = record.expireTime === -1 const isSurvival = isPermanent || currentTime < record.expireTime if (!isSurvival) { this.store.removeItem(key) } }) .then(() => { this.reloadWatchlist() }) }, /** * 清空关注列表 * @method cleanLocalData */ async cleanLocalData() { if (window.confirm('确定要清理所有关注吗?')) { await this.store.clear() alert('操作成功') } }, style: ` #cps__watchlist-panel .urlincontent:before { content: "["; vertical-align: 0.05em; padding: 0 0.15em; color: #bdb5ab; } #cps__watchlist-panel .urlincontent:after { content: "]"; vertical-align: 0.05em; padding: 0 0.15em; color: #bdb5ab; } .cps__watch-icon {position: relative;padding:0 1px;text-decoration:none;cursor:pointer;} .cps__watch-icon {text-decoration:none !important;} .cps__tab-header {height:40px} .cps__tab-header>span {margin-right:10px;padding:5px;cursor:pointer} .cps__tab-header .cps__tab-active,.cps__tab-header>span:hover {color:#591804;font-weight:700;border-bottom:3px solid #591804} .cps__tab-content {display:flex;justify-content:space-between;flex-wrap: wrap;} .cps__tab-content {display:none} .cps__tab-content.cps__tab-active {display:flex} .cps__list-panel {position:fixed;top:50px;left:50%;transform:translate(-50%, -50%);width:80%;overflow:auto;max-height:60%;background:#fff8e7;padding:15px 20px;border-radius:10px;box-shadow:0 0 10px #666;border:1px solid #591804;z-index:888;} .cps__list-panel .cps__list-c {width:100%;height:100%} .cps__list-panel .cps__list-c textarea {box-sizing:border-box;padding:0;margin:0;height:100%;width:100%;resize:none;} .cps__list-panel .cps__list-c > p:first-child {font-weight:bold;font-size:14px;margin-bottom:10px;} #cps__watchlist-panel p {margin-bottom:10px;} .cps__panel-close {position:absolute;top:5px;right:5px;padding:3px 6px;background:#fff0cd;color:#591804;transition:all .2s ease;cursor:pointer;border-radius:4px;text-decoration:none;z-index:888;} .cps__panel-close:hover {background:#591804;color:#fff0cd;text-decoration:none;} .cps__table {table-layout:fixed;width:100%;height:100%;border-top:1px solid #ead5bc;border-left:1px solid #ead5bc} .cps__table thead {background:#591804;border:1px solid #591804;color:#fff} .cps__table td,.cps__table th {padding:3px 5px;border-bottom:1px solid #ead5bc;border-right:1px solid #ead5bc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center} .cps__scroll-area {position:relative;height:100%;overflow:auto;border:1px solid #ead5bc} .cps__scroll-area::-webkit-scrollbar {width:6px;height:6px} .cps__scroll-area::-webkit-scrollbar-thumb {border-radius:10px;box-shadow:inset 0 0 5px rgba(0,0,0,.2);background:#591804} .cps__scroll-area::-webkit-scrollbar-track {box-shadow:inset 0 0 5px rgba(0,0,0,.2);border-radius:10px;background:#ededed} ` } // 辅助函数 ///////////////////////////////////////////////////// function debounce(fn, delay = 500) { let id let pendingPromise let resolvePending return (...args) => { clearTimeout(id) if (pendingPromise) { resolvePending(new Error("Debounced call cancelled")) } pendingPromise = new Promise((resolve) => { resolvePending = resolve }) id = setTimeout(async () => { try { const result = await fn(...args) resolvePending(result) } catch (err) { resolvePending(err) } finally { pendingPromise = null } }, delay) return pendingPromise } } function slideDown(el, duration = 400) { const display = window.getComputedStyle(el).display if (display === 'none') { el.style.display = 'block' } el.style.height = '0' const targetHeight = el.scrollHeight el.style.height = '' el.style.overflow = 'hidden' el.style.transition = `height ${duration}ms ease-out` el.style.height = `${targetHeight}px` el.addEventListener('transitionend', function handler() { el.style.removeProperty('height') el.style.removeProperty('overflow') el.style.removeProperty('transition') el.removeEventListener('transitionend', handler) }, { once: true }) } function fadeOut(el, duration = 400) { el.style.transition = `opacity ${duration}ms ease-out` el.style.opacity = '0' el.addEventListener('transitionend', function handler() { el.style.display = 'none' el.style.transition = '' el.style.opacity = '1' el.removeEventListener('transitionend', handler) }, { once: true }) } // 引擎 ///////////////////////////////////////////////////// class NGABBSScript_CheckPostStatus { constructor() { // 配置 this.setting = { original: [], normal: {}, advanced: {} } // 模块 this.modules = [] // 样式 this.style = '' // 数据存储 this.store = {} // 引用库 this.libs = {localforage} } /** * 获取模块对象 * @method getModule * @param {String} name 模块name * @return {Object} 模块对象 */ getModule(name) { for (const m of this.modules) { if (m.name && m.name === name) { return m } } return null } /** * 列表页渲染函数 * @method renderThreads */ renderThreads() { document.querySelectorAll('.topicrow:not([hld-cps-threads-render="ok"])').forEach(topicrow => { for (const module of this.modules) { try { module.renderThreadsFunc && module.renderThreadsFunc(topicrow, this) } catch (error) { this.printLog(`[${module.name}]模块在[renderThreadsFunc()]中运行失败!`) console.log(error) } } topicrow.setAttribute('hld-cps-threads-render', 'ok') }) } /** * 详情页渲染函数 * @method renderForms */ renderForms() { document.querySelectorAll('.forumbox.postbox:not([hld-cps-forms-render="ok"])').forEach(postbox => { if (!postbox.getElementsByClassName('small_colored_text_btn')) return // 等待NGA页面渲染完成 if (postbox.querySelectorAll('.small_colored_text_btn').length === 0) return for (const module of this.modules) { try { module.renderFormsFunc && module.renderFormsFunc(postbox, this) } catch (error) { this.printLog(`[${module.name}]模块在[renderFormsFunc()]中运行失败!`) console.log(error) } } postbox.setAttribute('hld-cps-forms-render', 'ok') }) } /** * 添加模块 * @method addModule * @param {Object} module 模块对象 * @param {Boolean} plugin 是否为插件 */ addModule(module) { // 组件预处理函数 if (module.preProcFunc) { try { module.preProcFunc(this) } catch (error) { this.printLog(`[${module.name}]模块在[preProcFunc()]中运行失败!`) console.log(error) } } // 添加设置 const addSetting = setting => { // 标准模块配置 if (setting.shortCutCode && this.setting.normal.shortcutKeys) { this.setting.normal.shortcutKeys.push(setting.shortCutCode) } if (setting.key) { this.setting[setting.type || 'normal'][setting.key] = setting.default ?? '' this.setting.original.push(setting) } } // 功能板块 if (module.setting && !Array.isArray(module.setting)) { addSetting(module.setting) } if (module.settings && Array.isArray(module.settings)) { for (const setting of module.settings) { addSetting(setting) } } // 添加样式 if (module.style) { this.style += module.style } this.modules.push(module) } /** * 判断当前页面是否为列表页 * @method isThreads * @return {Boolean} 判断状态 */ isThreads() { return !!document.getElementById('m_threads') } /** * 判断当前页面是否为详情页 * @method isForms * @return {Boolean} 判断状态 */ isForms() { return !!document.getElementById('m_posts') } /** * 抛出异常 * @method throwError * @param {String} msg 异常信息 */ throwError(msg) { alert(msg) throw(msg) } /** * 初始化 * @method init */ init() { // 开始初始化 this.printLog('初始化...') localforage.config({name: 'NGA BBS Script DB'}) const startInitTime = new Date().getTime() const modulesTable = [] //同步配置 this.loadSetting() // 组件初始化函数 for (const module of this.modules) { if (module.initFunc) { try { module.initFunc(this) } catch (error) { this.printLog(`[${module.name}]模块在[initFunc()]中运行失败!`) console.log(error) } } } // 组件后处理函数 for (const module of this.modules) { if (module.postProcFunc) { try { module.postProcFunc(this) } catch (error) { this.printLog(`[${module.name}]模块在[postProcFunc()]中运行失败!`) console.log(error) } } } // 动态样式 for (const module of this.modules) { if (module.asyncStyle) { try { this.style += module.asyncStyle(this) } catch (error) { this.printLog(`[${module.name}]模块在[asyncStyle()]中运行失败!`) console.log(error) } } modulesTable.push({ name: module.title || module.name || 'UNKNOW', type: module.type == 'plugin' ? '插件' : '标准模块', version: module.version || '-' }) } // 插入样式 const styleEl = document.createElement("style") styleEl.appendChild(document.createTextNode(this.style)) document.getElementsByTagName('head')[0].appendChild(styleEl) // 初始化完成 const endInitTime = new Date().getTime() console.table(modulesTable) this.printLog(`[v${this.getInfo().version}] 初始化完成: 共加载${this.modules.length}个模块,总耗时${endInitTime-startInitTime}ms`) console.log('%c反馈问题请前往: https://github.com/stone5265/GreasyFork-NGA-Check-Post-Status/issues', 'color:orangered;font-weight:bolder') } /** * 通知弹框 * @method popNotification * @param {String} msg 消息内容 * @param {Number} duration 显示时长(ms) */ popNotification(msg, duration = 1000) { let container = document.getElementById('cps__noti-container') if (!container) { container = document.createElement('div') container.id = 'cps__noti-container' document.body.appendChild(container) } const msgEl = document.createElement('div') msgEl.className = 'cps__noti-msg' msgEl.textContent = msg msgEl.style.display = 'none' container.appendChild(msgEl) slideDown(msgEl, 100) setTimeout(() => { fadeOut(msgEl, 500) }, duration) setTimeout(() => { msgEl.remove() }, duration + 500) } /** * 消息弹框 * @method popMsg * @param {String} msg 消息内容 * @param {String} type 消息类型 [ok, err, warn] */ popMsg(msg, type='ok') { const msgEl = document.createElement('div') msgEl.className = `cps__msg cps__msg-${type}` msgEl.textContent = msg msgEl.style.display = 'none' document.querySelectorAll('.cps__msg').forEach(el => el.remove()) document.body.appendChild(msgEl) slideDown(msgEl, 200) setTimeout(() => { fadeOut(msgEl, 500) }, type == 'ok' ? 2000 : 5000) setTimeout(() => { msgEl.remove() }, type == 'ok' ? 2500 : 5500) } /** * 打印控制台消息 * @method printLog * @param {String} msg 消息内容 */ printLog(msg) { console.log(msg) } /** * 读取值 * @method saveSetting * @param {String} key */ getValue(key) { try { return GM_getValue(key) } catch {} } /** * 写入值 * @method setValue * @param {String} key * @param {String} value */ setValue(key, value) { try { GM_setValue(key, value) } catch {} } /** * 删除值 * @method deleteValue * @param {String} key */ deleteValue(key) { try { GM_deleteValue(key) } catch {} } /** * 保存配置到本地 * @method saveSetting * @param {String} msg 自定义消息信息 */ saveSetting(msg='保存配置成功,刷新页面生效') { for (let k in this.setting.advanced) { const inputEl = document.getElementById('cps__adv-' + k) if (inputEl) { const originalSetting = this.setting.original.find(s => s.type == 'advanced' && s.key == k) const valueType = typeof originalSetting.default const inputType = inputEl.tagName if (inputType === 'SELECT') { this.setting.advanced[k] = inputEl.value } else { if (valueType === 'boolean') { this.setting.advanced[k] = inputEl.checked } if (valueType === 'number') { this.setting.advanced[k] = Math.max( Number(inputEl.value), originalSetting.min || 0 ); } if (valueType === 'string') { this.setting.advanced[k] = inputEl.value } } } } script.setValue('hld_cps_NGA_advanced_setting', JSON.stringify(this.setting.advanced)) msg && this.popMsg(msg) } /** * 从本地读取配置 * @method loadSetting */ loadSetting() { try { const advancedSettingStr = script.getValue('hld_cps_NGA_advanced_setting') if (advancedSettingStr) { let localAdvancedSetting = JSON.parse(advancedSettingStr) for (let k in this.setting.advanced) { !localAdvancedSetting.hasOwnProperty(k) && (localAdvancedSetting[k] = this.setting.advanced[k]) } for (let k in localAdvancedSetting) { !this.setting.advanced.hasOwnProperty(k) && delete localAdvancedSetting[k] } this.setting.advanced = localAdvancedSetting } } catch(e) { script.throwError(`读取配置文件出现错误,无法加载配置文件!\n错误问题: ${e}\n\n请尝试使用【修复脚本】来修复此问题`) } } /** * 创建储存对象实例 * @param {String} instanceName 实例名称 */ createStorageInstance(instanceName) { if (!instanceName || Object.keys(this.store).includes(instanceName)) { this.throwError('创建储存对象实例失败,实例名称不能为空或实例名称已存在') } const lfInstance = localforage.createInstance({name: instanceName}) this.store[instanceName] = lfInstance return lfInstance } /** * 运行脚本 * @method run */ run() { this.init() setInterval(() => { this.isThreads() && this.renderThreads() this.isForms() && this.renderForms() }, 500) } /** * 获取脚本信息 * @method getInfo * @return {Object} 脚本信息对象 */ getInfo() { return { version: GM_info.script.version, author: 'stone5265', github: 'https://github.com/stone5265/GreasyFork-NGA-Check-Post-Status', } } } /** * 设置模块 * @name SettingPanel * @description 提供脚本的设置面板,提供配置修改,保存等基础功能 */ const SettingPanel = { name: 'SettingPanel', title: '设置模块', initFunc() { //设置面板 const panelEl = document.createElement('div') panelEl.id = 'cps__setting-cover' panelEl.className = 'animated zoomIn' panelEl.innerHTML = ` <div id="cps__setting-panel"> <a href="javascript:void(0)" id="cps__setting-close" class="cps__setting-close" close-type="hide">×</a> <p class="cps__sp-title">NGA检查帖子可见状态<span class="cps__script-info">v${script.getInfo().version}</span><span class="cps__script-info"> - 基于NGA优化摸鱼体验v4.5.4引擎</span></p> <div style="clear:both"></div> <div class="cps__advanced-setting"> <div class="cps__advanced-setting-panel"> <p>⚠ 鼠标停留在<span class="cps__help" title="详细描述">选项文字</span>上可以显示详细描述,设置有误可能会导致插件异常或者无效!</p> <table id="cps__advanced_left"></table> <table id="cps__advanced_right"></table> </div> </div> <div class="cps__buttons"> <span></span> <span> <button class="cps__btn" id="cps__reset-data">重置为默认设置</button> <button class="cps__btn" id="cps__save-data">保存设置</button> </span> </div> </div> ` const insertDom = setting => { if (setting.type === 'advanced') { let formItem = '' const valueType = typeof setting.default if (valueType === 'boolean') { formItem = `<input type="checkbox" id="cps__adv-${setting.key}">` } if (valueType === 'number') { formItem = `<input type="number" min="${setting.min || 0}" oninput="this.value = this.value.replace(/\\./g, '');" id="cps__adv-${setting.key}">` } if (valueType === 'string') { if (setting.options) { let t = '' for (const option of setting.options) { t += `<option value="${option.value}">${option.label}</option>` } formItem = `<select id="cps__adv-${setting.key}">${t}</select>` } else { formItem = `<input type="text" id="cps__adv-${setting.key}">` } } const table = panelEl.querySelector(`#cps__advanced_${setting.menu || 'left'}`) table.insertAdjacentHTML('beforeend', ` <tr> <td><span class="cps__help" help="${setting.desc || ''}">${setting.title || setting.key}</span></td> <td>${formItem}</td> </tr>`) } } for (const module of script.modules) { if (module.setting && module.setting.key) { insertDom(module.setting) } if (module.settings) { for (const setting of module.settings) { setting.key && insertDom(setting) } } } document.body.appendChild(panelEl) //高级设置 for (let k in script.setting.advanced) { const inputEl = document.getElementById('cps__adv-' + k) if (inputEl) { const valueType = typeof script.setting.advanced[k] if (valueType === 'boolean') { inputEl.checked = script.setting.advanced[k] } else if (valueType === 'number' || valueType === 'string') { inputEl.value = script.setting.advanced[k] } } } // 提示信息Tips document.body.addEventListener('mouseover', function(e) { if (e.target.classList.contains('cps__help')) { const helpEl = e.target const helpText = helpEl.getAttribute('help') if (!helpText) return const tipEl = document.createElement('div') tipEl.className = 'cps__help-tips' tipEl.innerHTML = helpText.replace(/\n/g, '<br>') // 定位提示框 const rect = helpEl.getBoundingClientRect() tipEl.style.position = 'absolute' tipEl.style.top = (rect.bottom + window.scrollY + 5) + 'px' tipEl.style.left = (rect.left + window.scrollX) + 'px' document.body.appendChild(tipEl) } }) document.body.addEventListener('mouseout', function(e) { if (e.target.classList.contains('cps__help')) { document.querySelectorAll('.cps__help-tips').forEach(el => el.remove()) } }) // 关闭设置面板 document.body.addEventListener('click', function(e) { if (e.target.classList.contains('cps__setting-close')) { const closeType = e.target.getAttribute('close-type') const panel = e.target.parentElement if (closeType === 'hide') { panel.style.display = 'none' panel.parentElement.style.display = 'none' } else { panel.remove() } } }) // 保存配置 document.getElementById('cps__save-data').addEventListener('click', function() { script.saveSetting('') CheckPostStatus.reloadAutoCheckInfo() fadeOut(document.getElementById('cps__setting-cover'), 200) }) // 重置为默认设置 document.getElementById('cps__reset-data').addEventListener('click', function() { if (window.confirm('重置为默认设置,这会清除脚本的大部分数据\n* 数据包含配置,上一次自动检查时间\n(不包括关注列表)\n* 此操作不可逆转,请谨慎操作\n\n继续请点击【确定】')) { try { GM_listValues().forEach(key => GM_deleteValue(key)) } catch {} alert('操作成功,请刷新页面重试') } }) }, style: ` .animated {animation-duration:.3s;animation-fill-mode:both;} .animated-1s {animation-duration:1s;animation-fill-mode:both;} .zoomIn {animation-name:zoomIn;} .bounce {-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom;} .fadeInUp {-webkit-animation-name:fadeInUp;animation-name:fadeInUp;} #loader {display:none;position:absolute;top:50%;left:50%;margin-top:-10px;margin-left:-10px;width:20px;height:20px;border:6px dotted #FFF;border-radius:50%;-webkit-animation:1s loader linear infinite;animation:1s loader linear infinite;} @keyframes loader {0% {-webkit-transform:rotate(0deg);transform:rotate(0deg);}100% {-webkit-transform:rotate(360deg);transform:rotate(360deg);}} @keyframes zoomIn {from {opacity:0;-webkit-transform:scale3d(0.3,0.3,0.3);transform:scale3d(0.3,0.3,0.3);}50% {opacity:1;}} @keyframes bounce {from,20%,53%,80%,to {-webkit-animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);}40%,43% {-webkit-animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0);}70% {-webkit-animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0);}90% {-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0);}} @keyframes fadeInUp {from {opacity:0;-webkit-transform:translate3d(-50%,100%,0);transform:translate3d(-50%,100%,0);}to {opacity:1;-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0);}} .cps__msg{display:none;position:fixed;top:10px;left:50%;transform:translateX(-50%);color:#fff;text-align:center;z-index:99996;padding:10px 30px 10px 45px;font-size:16px;border-radius:10px;} .cps__msg a{color:#fff;text-decoration: underline;} .cps__msg-ok{background:#4bcc4b} .cps__msg-err{background:#c33} .cps__msg-warn{background:#FF9900} #cps__noti-container {position:fixed;top:10px;left:10px;z-index:99;} .cps__noti-msg {display:none;padding:10px 20px;font-size:14px;font-weight:bold;color:#fff;margin-bottom:10px;background:rgba(0,0,0,0.6);border-radius:10px;cursor:pointer;} .cps__btn-groups {display:flex;justify-content:center !important;margin-top:10px;} button.cps__btn {padding:3px 8px;border:1px solid #591804;background:#fff8e7;color:#591804;} button.cps__btn:hover {background:#591804;color:#fff0cd;} button.cps__btn[disabled] {opacity:.5;} .cps__script-info {margin-left:4px;font-size:70%;color:#666;} #cps__setting {color:#6666CC;cursor:pointer;} #cps__setting-cover {display:none;padding-top: 70px;position:absolute;top:0;left:0;right:0;bottom:0;z-index:999;} #cps__setting-panel {position:relative;background:#fff8e7;width:700px;left: 50%;transform: translateX(-50%);padding:15px 20px;border-radius:10px;box-shadow:0 0 10px #666;border:1px solid #591804;} #cps__setting-panel p {margin-bottom:10px;} #cps__setting-panel .cps__sp-title {font-size:15px;font-weight:bold;text-align:center;} #cps__setting-panel .cps__sp-section {font-weight:bold;margin-top:20px;} .cps__setting-close {position:absolute;top:5px;right:5px;padding:3px 6px;background:#fff0cd;color:#591804;transition:all .2s ease;cursor:pointer;border-radius:4px;text-decoration:none;z-index:9999;} .cps__setting-close:hover {background:#591804;color:#fff0cd;text-decoration:none;} #cps__setting-panel button {transition:all .2s ease;cursor:pointer;} .cps__advanced-setting {border-top: 1px solid #e0c19e;border-bottom: 1px solid #e0c19e;padding: 3px 0;margin-top:25px;} .cps__advanced-setting >span {font-weight:bold} .cps__advanced-setting >button {padding: 0px;margin-right:5px;width: 18px;text-align: center;} .cps__advanced-setting-panel {display:flex;padding:5px 0;flex-wrap: wrap;} .cps__advanced-setting-panel>p {width:100%;} .cps__advanced-setting-panel>table {width:50%;} .cps__advanced-setting-panel>p {margin: 7px 0 !important;font-weight:bold;} .cps__advanced-setting-panel>p svg {height:16px;width:16px;vertical-align: top;margin-right:3px;} .cps__advanced-setting-panel>table td {padding-right:10px} .cps__advanced-setting-panel input[type=text],.cps__advanced-setting-panel input[type=number] {width:80px} .cps__advanced-setting-panel input[type=number] {border: 1px solid #e6c3a8;box-shadow: 0 0 2px 0 #7c766d inset;border-radius: 0.25em;} .cps__buttons {clear:both;display:flex;justify-content:space-between;padding-top:15px;} button.cps__btn {padding:3px 8px;border:1px solid #591804;background:#fff8e7;color:#591804;} button.cps__btn:hover {background:#591804;color:#fff0cd;} .cps__sp-fold {padding-left:23px;} .cps__help {cursor:help;text-decoration: underline;} .cps__help-tips {position: absolute;padding: 5px 10px;background: rgba(0,0,0,.8);color: #FFF;border-radius: 5px;z-index: 9999;} ` } // 初始化脚本 const script = new NGABBSScript_CheckPostStatus() // 添加模块 script.addModule(SettingPanel) script.addModule(CheckPostStatus) // 运行脚本 script.run() })()