// ==UserScript==
// @name NGA优化摸鱼体验-帖子浏览记录
// @namespace https://github.com/kisshang1993/NGA-BBS-Script/tree/master/plugins/PostReadingRecord
// @version 1.1.1
// @author HLD
// @description 记录帖子的阅读状态,着色以阅读帖子标题,跟踪后续新回复数量
// @license MIT
// @match *://bbs.nga.cn/*
// @match *://ngabbs.com/*
// @match *://nga.178.com/*
// @match *://g.nga.cn/*
// @grant unsafeWindow
// @run-at document-start
// @inject-into content
// ==/UserScript==
(function (registerPlugin) {
'use strict';
const PostReadingRecord = {
name: 'PostReadingRecord',
title: '帖子浏览记录',
desc: '记录帖子的阅读状态,着色以阅读帖子标题,跟踪后续新回复数量',
settings: [{
key: 'markColor',
title: '标记着色颜色',
default: '#000000'
}, {
key: 'markOpacity',
title: '颜色不透明度',
desc: '0~100之间的数字,0为完全透明,100为完全不透明',
default: 50
}, {
key: 'replyCountEnable',
title: '显示新增回复',
default: true
}, {
key: 'replyCountStyle',
title: '新增回复样式',
default: 'tag',
options: [{
label: '绿色标签',
value: 'tag'
}, {
label: '极简文本',
value: 'text'
}]
}, {
key: 'expireDays',
title: '记录过期天数',
desc: '记录帖子状态的数据过期天数,数据过期后将会自动删除状态数据,用于释放储存空间,-1为永不过期',
default: 90
}],
buttons: [{
title: '清理所有记录数据',
action: 'cleanLocalData'
}],
store: null,
beforeSaveSettingFunc(settings) {
const $ = this.mainScript.libs.$
if (!$.isNumeric(settings['markOpacity'])) {
return '颜色不透明度必须是个数字'
}
if (!$.isNumeric(settings['expireDays'])) {
return '记录过期天数必须是个数字'
}
},
initFunc() {
const $ = this.mainScript.libs.$
// 创建储存实例
this.store = this.mainScript.createStorageInstance('NGA_BBS_Script__PostReadingRecord')
// 调用标准模块authorMark初始化颜色选择器
this.mainScript.getModule('AuthorMark').initSpectrum(`[plugin-id="${this.pluginID}"][plugin-setting-key="markColor"]`)
// 初始化的时候根据设置清理超过一定时间的数据,避免无限增长数据
const currentTime = Math.ceil(new Date().getTime() / 1000)
const expireTime = this.pluginSettings['expireDays'] == -1 ? -1 : this.pluginSettings['expireDays'] * 24 * 60 * 60
if (expireTime > -1) {
let removedCount = 0
this.store.iterate((value, key) => {
if (currentTime - value.lastReadTime >= expireTime) {
this.store.removeItem(key)
removedCount += 1
}
})
.then(() => {
this.mainScript.printLog(`${this.title}: 已清除${removedCount}条超期(>${this.pluginSettings['expireDays']}d)数据`)
})
.catch(err => {
console.error(`${this.title}清除超期数据失败,错误原因:`, err)
})
}
// 自动记录阅读位置
$(window).on('scroll resize', () => {
if (!this.mainScript.isForms()) return
this.calcReadCount()
})
// 绑定继续阅览事件
$(document).on('click', '.hld__unread-label', function() {
unsafeWindow.__LOADERREAD.go(1, {
fromUrl: window.location.href,
url: $(this).attr('data-continueurl')
})
})
},
async renderThreadsFunc($el) {
const markStyle = `color: ${this.pluginSettings['markColor']}; opacity: ${parseInt(this.pluginSettings['markOpacity']) / 100};`
// 提取数据
const $a = $el.find('.c1 > a')
const tid = this.mainScript.getModule('AuthorMark').getQueryString('tid', $a.attr('href'))
const currentCount = parseInt($a.text())
if (!tid || isNaN(currentCount)) return
// 检查并标记已读帖子
const record = await this.store.getItem(tid)
const recordCount = record?.lastReadCount || -1
if (record) {
$el.find('.c2 > a').attr('style', markStyle)
}
if (this.pluginSettings['replyCountEnable'] && recordCount > -1 && currentCount > recordCount) {
const url = `https://${window.location.host}${$a.attr('href')}&page=${record.lastReadPage}#anchorid=${record.lastReadCount}`
$el.find('.c2 > span[id^=t_pc]').append(`
<span data-continueurl="${url}" class="hld__help hld__unread-label" help="上次阅读位置">
<span class="hld__new-reply-count-${this.pluginSettings['replyCountStyle']}">
${currentCount - recordCount}
</span>
</span>
`)
}
},
renderFormsFunc($el) {
if ($el.index() == 0) {
this.calcReadCount()
}
},
renderAlwaysFunc() {
if (!this.mainScript.isForms()) return
const $ = this.mainScript.libs.$
const [originalURL, anchorid] = window.location.href.split('#anchorid=')
if (anchorid && !isNaN(parseInt(anchorid))) {
// 滚动至锚点处
for (let i=parseInt(anchorid)-1;i>0;i--) {
const $posterinfo = $(`#post1strow${anchorid}`)
if ($posterinfo.length > 0) {
$posterinfo.parents('table.forumbox.postbox')
const $parentrow = $posterinfo.parents('table.forumbox.postbox')
if (!$parentrow.next().hasClass('hld__readnow')) {
$parentrow.after(`<div class="hld__readnow">📌 上次阅读位置 📌</div>`)
}
history.replaceState(null, null, originalURL)
setTimeout(() => {
$(window).scrollTop($posterinfo.offset().top + $posterinfo.height() - $(window).height() + 100)
}, 500)
break
}
}
}
},
// 计算阅读位置
calcReadCount() {
const $ = this.mainScript.libs.$
$('.forumbox.postbox').each(async (index, dom) => {
const $el = $(dom)
const currentPostboxID = $el.find('tr[id^=post1strow]').attr('id')
const currentTid = unsafeWindow.__CURRENT_TID + ''
if (!currentPostboxID || !currentTid) return
const currentRecord = await this.store.getItem(currentTid)
const lastReadCount = currentRecord?.lastReadCount ?? 0
const currentReadCount = parseInt(currentPostboxID.substring(10)) + 1
const currentElTop = $el.offset().top
const currentElBottom = currentElTop + $el.outerHeight()
const viewportTop = $(window).scrollTop()
const viewportBottom = viewportTop + $(window).height()
if (currentElTop < viewportBottom && currentElBottom > viewportTop) {
if (currentReadCount > lastReadCount) {
const currentPage = this.mainScript.getModule('AuthorMark').getQueryString('page') ?? 1
this.store.setItem(currentTid, {
lastReadCount: currentReadCount,
lastReadPage: parseInt(currentPage),
lastReadTime: Math.ceil(new Date().getTime() / 1000)
})
}
}
})
},
cleanLocalData() {
if (window.confirm('确定要清理所有记录数据吗?')) {
this.store.clear()
alert('操作成功,请刷新页面重试')
}
},
style: `
.hld__unread-label {cursor: pointer;}
.hld__unread-label:hover:after {content: '🚀';}
.hld__new-reply-count-tag {margin-left: 10px;display: inline-block;background-color: #8BC34A;border-radius: 10px;padding: 0 10px;color: #FFF;transform: scale(.9);}
.hld__new-reply-count-tag:before {content: '未阅读:'}
.hld__new-reply-count-tag:after {content: '条'}
.hld__new-reply-count-text {margin-left: 10px;display: inline-block;}
.hld__new-reply-count-text:before {content: '+'}
.hld__readnow {text-align: center;height:20px;line-height:20px;background:#607d8b;color:#FFF;}
`
}
registerPlugin(PostReadingRecord)
})(function(plugin) {
plugin.meta = GM_info.script
unsafeWindow.ngaScriptPlugins = unsafeWindow.ngaScriptPlugins || []
unsafeWindow.ngaScriptPlugins.push(plugin)
});