// ==UserScript==
// @name 場外大樓過濾器
// @description 已經厭倦看到一堆不感興趣的大樓? 這就是你需要的
// @namespace nathan60107
// @author nathan60107(貝果)
// @version 1.1.3
// @homepage https://home.gamer.com.tw/creationCategory.php?owner=nathan60107&c=425332
// @match https://forum.gamer.com.tw/B.php?*bsn=60076*
// @icon https://www.google.com/s2/favicons?sz=64&domain=gamer.com.tw
// @require https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require https://code.jquery.com/jquery-3.6.3.min.js
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// @noframes
// ==/UserScript==
let $ = jQuery
let dd = (...d) => {
if (window.BAHAID && window.BAHAID !== 'nathan60107') return
d.forEach((it) => { console.log(it) })
}
/**
* @param {boolean} bool
*/
function isCheck(bool) {
return bool ? 'checked' : ''
}
/**
* @param {string} condi
* @param {string} curr
*/
function isSelected(condi, curr) {
return condi === curr ? 'selected' : ''
}
/**
* @param {string|undefined} a
* @param {string} b
*/
function versionCompare(a, b) {
if (!a) return -1
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
}
/**
* @param {number} reply
* @param {number} like
*/
function userCondition(reply, like) {
if (!setting.filterReply && !setting.filterLike) {
return false
} else if (!setting.filterLike) {
return reply > setting.replyLimit
} else if (!setting.filterReply) {
return like > setting.likeLimit
} else {
return [
reply > setting.replyLimit,
like > setting.likeLimit,
].reduce(
setting.likeCondition === 'or'
? (a, b) => a || b : (a, b) => a && b
)
}
}
/** @param {{title: string, snA: string}[]} whitelist */
function whitelistToString(whitelist) {
return whitelist.map(wl =>
`<li>
-
<a href="https://forum.gamer.com.tw/C.php?bsn=60076&snA=${wl.snA}">${wl.title}</a>
<button data-snA="${wl.snA}">刪除</button>
</li>`
).join('')
}
/**
* @type {defaultSetting}
*/
let setting = {}
const defaultSetting = {
dataVersion: GM_info.script.version,
enableLoadMore: true,
enableFilter: true,
filterReply: true,
replyLimit: 1000,
filterLike: false,
likeCondition: 'and',
likeLimit: 1000,
/** @type {{title: string, snA: string}[]} */
whitelist: []
}
const settingKey = Object.keys(defaultSetting)
let postSet = new Set()
let removeCount = 0
let resetCoundown = -1
let url = new URLSearchParams(window.location.href)
let pageIndex = +(url.get('page') ?? 1)
function initSettingPanel() {
for (const key of settingKey) {
setting[key] = GM_getValue(key)
if (setting[key] === undefined) {
setting[key] = defaultSetting[key]
GM_setValue(key, defaultSetting[key])
}
}
if (versionCompare(setting.dataVersion, '1.1.0') < 0) {
resetSetting(false)
toastr.info('由於版本更新,設定已重置,若持續出現此通知請回報錯誤。')
}
if (setting.enableLoadMore) $('.b-popular, .b-pager').hide()
$('.BH-qabox1').after(`
<style>
#no-building, #no-building .setting-building, #no-building ul {
display: flex;
flex-direction: column;
gap: 10px;
}
#no-building {
padding: 18px 12px;
}
#no-building .setting-building {
padding-left: 10px;
}
#no-building label {
display: block;
}
#no-building input[type="number"] {
width: 4em;
}
#no-building button, #no-building select {
width: max-content;
}
#no-building .setting-building select {
margin-left: 45px;
}
#no-building input::-webkit-outer-spin-button,
#no-building input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#no-building input[type=number] {
-moz-appearance: textfield;
}
</style>
<h5>大樓過濾設定</h5>
<div id="no-building" class="BH-rbox"></div>
`)
updateSettingPanel()
}
function updateSettingPanel() {
$('#no-building').html(`
<div class="version">腳本版本:${GM_info.script.version}</div>
<div class="page">目前頁數:${pageIndex}</div>
<div class="info">已過濾大樓: ${removeCount} 棟</div>
<label>
<input ${isCheck(setting.enableLoadMore)} data-key="enableLoadMore" type="checkbox" >
啟用載入更多內容
</label>
<label>
<input ${isCheck(setting.enableFilter)} data-key="enableFilter" type="checkbox" >
啟用大樓過濾
</label>
大樓過濾條件:
<div class="setting-building">
<label>
<input ${isCheck(setting.filterReply)} data-key="filterReply" type="checkbox">
回覆超過<input value="${setting.replyLimit}" data-key="replyLimit" type="number">樓
</label>
<select data-key="likeCondition">
<option value="and" ${isSelected(setting.likeCondition, 'and')}>且</option>
<option value="or" ${isSelected(setting.likeCondition, 'or')}>或</option>
</select>
<label>
<input ${isCheck(setting.filterLike)} data-key="filterLike" type="checkbox">
推文超過<input value="${setting.likeLimit}" data-key="likeLimit" type="number">次
</label>
白名單文章:
<ul>
${whitelistToString(setting.whitelist)}
</ul>
<input class="setting-whitelist-input" placeholder="在此處貼上文章網址" type="text">
<button class="setting-add-whitelist" type="button">新增白名單</button>
</div>
<button class="setting-reset" type="button">${resetCoundown === -1 ? '重置設定' : `確定? ${resetCoundown}`}</button>
`)
$('#no-building .setting-reset').on('click', confirmReset)
$('#no-building .setting-add-whitelist').on('click', () => modifyWhitelist('add'))
for (const { snA } of setting.whitelist) {
$(`[data-snA="${snA}"]`).on('click', () => modifyWhitelist(snA))
}
for (const [key, val] of Object.entries(defaultSetting)) {
if (Array.isArray(val)) continue
let targetKey = typeof val === 'boolean' ? 'checked' : 'value'
$(`#no-building [data-key="${key}"]`).on('change', function (e) {
setting[key] = e.target[targetKey]
if (typeof val === 'number') setting[key] = parseInt(setting[key])
saveSetting()
})
}
}
function saveSetting() {
for (const key of settingKey) {
GM_setValue(key, setting[key])
}
toastr.info('大樓過濾設定已更新')
}
function confirmReset() {
// First click of reset setting, countdown
if (resetCoundown === -1) {
resetCoundown = 10
updateSettingPanel()
let itv = setInterval(() => {
resetCoundown--
updateSettingPanel()
if (resetCoundown === -1) clearInterval(itv)
}, 1000)
// Second click, reset setting and reload
} else {
resetSetting()
}
}
function resetSetting(reload = true) {
for (const key of settingKey) {
GM_setValue(key, defaultSetting[key])
}
if (reload) location.reload()
}
/**
* If action is add, add url in input to whitelist.
* Or, delete whiltelist item that snA == action
* @param {'add'|string} action
*/
function modifyWhitelist(action) {
if (action === 'add') {
let tempList = _.cloneDeep(setting.whitelist)
let input = $('.setting-whitelist-input').val().match(/snA=(\d+)/)
if (!input) return
let snA = input[1]
$.ajax({
url: `https://forum.gamer.com.tw/C.php?bsn=60076&snA=${snA}`
}).done((resHTML) => {
let title = $(resHTML).filter('title').text()
.replace(' @場外休憩區 哈啦板 - 巴哈姆特', '')
.replace(/^【.*】/, '')
tempList.push({ title, snA })
if (_.isEqual(setting.whitelist, tempList)) {
toastr.warning('白名單已存在')
} else {
setting.whitelist = _.uniqBy(tempList, 'snA')
updateSettingPanel()
saveSetting()
}
})
} else {
_.pullAllBy(setting.whitelist, [{ snA: action }], 'snA')
updateSettingPanel()
saveSetting()
}
}
/**
* @param {HTMLElement} html
* @param {'append'|'remove'} mode
*/
function processHtml(html, mode) {
let snA = $(html).find('.b-list__time__edittime > a').attr('href').match(/snA=([\d]+)/)[1]
let title = $(html).find('.b-list__main__title').text()
let top = $(html).hasClass('b-list__row--sticky')
let like = $(html).find('.b-list__summary__gp').text()
let replyRaw = $(html).find('.b-list__time__edittime > a').attr('href').match(/tnum=([0-9]+)&/)
let reply = replyRaw ? parseInt(replyRaw[1]) - 1 : 0
let inWhitelist = _.filter(setting.whitelist, ['snA', snA]).length !== 0
// Duplicated, skip it
if (postSet.has(snA)) return
// Should be filtered
if (setting.enableFilter && !top && !inWhitelist &&
userCondition(reply, like)) {
if (mode === 'remove') $(html).remove()
removeCount++
// Should be kept
} else {
if (mode === 'append') {
aTag = html.querySelector('.b-list__main a')
aTag.href = aTag.href.replace(/&tnum=\d+&bPage=\d+/, '')
$('form[method="post"] > .b-list-wrap > .b-list > tbody').append(html)
}
}
postSet.add(snA)
}
function htmlToPost() {
$('.b-list__row:not(:has(.b-list_ad))').map(
function f() { processHtml(this, 'remove') }
)
updateSettingPanel()
}
/**
* @type {IntersectionObserverCallback}
*/
function loadMore(entries) {
if (!setting.enableLoadMore || entries.every((val) => !val.isIntersecting)) return
pageIndex++
url.set('page', pageIndex)
$.ajax({
url: decodeURIComponent(url.toString())
}).done((resHTML) => {
$(resHTML).find('.b-list__row:not(:has(.b-list_ad))').map(
function f() { processHtml(this, 'append') }
)
updateSettingPanel()
// Register lazyload img and draw non-image thumbnail
Forum.B.lazyThumbnail()
Forum.Common.drawNoImageCanvas()
})
}
function initIntersectionObserver() {
let observer = new IntersectionObserver(loadMore)
observer.observe($('#BH-footer')[0])
}
(function () {
document.addEventListener("DOMContentLoaded", function () {
initIntersectionObserver()
initSettingPanel()
htmlToPost()
})
})();
/**
* Reference:
* [JSDoc](https://ricostacruz.com/til/typescript-jsdoc)
*/