Mobilism: New releases quick lookup, unpaginated compact listing & filtering

Applies filering and endless compact listing of previously unread articles in Releases section. Makes browsing through newly added releases since last visit much more quicker. Supports adding ignore rule for each listed release.

当前为 2021-03-31 提交的版本,查看 最新版本

// ==UserScript==
// @name         Mobilism: New releases quick lookup, unpaginated compact listing & filtering
// @namespace    https://gf.qytechs.cn/users/321857-anakunda
// @version      1.00.0
// @description  Applies filering and endless compact listing of previously unread articles in Releases section. Makes browsing through newly added releases since last visit much more quicker. Supports adding ignore rule for each listed release.
// @author       Anakunda
// @copyright    2021, Anakunda (https://gf.qytechs.cn/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @match        https://forum.mobilism.org/portal.php?mode=articles&block=aapp*
// @match        https://forum.mobilism.me/portal.php?mode=articles&block=aapp*
// @connect      *
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @require      https://openuserjs.org/src/libs/Anakunda/xhrLib.js
// ==/UserScript==

'use strict';

let lastId = GM_getValue('latest_read'), ignoreRules = GM_getValue('ignore_rules', [ ]), filtered = false;

function addFilter(title) {
	let modal = document.createElement('div');
	modal.style = 'position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #0008;' +
		'opacity: 0; transition: opacity 0.15s linear;';
	modal.innerHTML = `
<form id="add-rule-form" style="background-color: darkslategray; font-size-adjust: 0.75; position: absolute; top: 30%; left: 10%; border-radius: 0.5em; padding: 20px 30px;">
	<div style="color: white; margin-bottom: 3em; font-size-adjust: 1; font-weight: bold;">Add rule as</div>
	<label style="color: white; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;">
		<input name="rule-type" type="radio" value="plaintext" checked="true' style="margin: 5px 5px 5px 0; cursor: pointer;" />
		Plain text
	</label>
	<label style="margin-left: 2em; color: white; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;">
		<input name="rule-type" type="radio" value="regexp" style="margin: 5px 5px 5px 0px; cursor: pointer;" />
		Regular expression
	</label>
	<br>
	<label style="color: white; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;">
		Expression:
		<input name="expression" type="text" style="width: 35em; height: 1.6em; font-size-adjust: 0.75; margin-left: 5px; margin-top: 1em;" />
	</label>
	<br>
	<label style="color: white; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;">
		<input name="ignore-case" type="checkbox" checked="true" style="margin: 1em 5px 0 0; cursor: pointer;" />
		Ignore case
	</label>
	<br>
	<input id="btn-cancel" type="button" value="Cancel" style="margin-top: 3em; float: right; padding: 2px 7px; font-size-adjust: 0.65;" />
	<input id="btn-add" type="button" value="Add rule" style="margin-top: 3em; float: right; padding: 2px 7px; font-size-adjust: 0.65; margin-right: 1em;" />
</form>
`;
	document.body.append(modal);
	let form = document.getElementById('add-rule-form'),
			radioPlain = form.querySelector('input[type="radio"][value="plaintext"]'),
			radioRegExp = form.querySelector('input[type="radio"][value="regexp"]'),
			expression = form.querySelector('input[type="text"][name="expression"]'),
			chkCaseless = form.querySelector('input[type="checkbox"][name="ignore-case"]'),
			btnAdd = form.querySelector('input#btn-add'),
			btnCancel = form.querySelector('input#btn-cancel');
	if ([form, btnAdd, btnCancel, radioPlain, radioRegExp, expression, chkCaseless].some(elem => elem == null)) {
		console.warn('Dialog creation error');
		return;
	}
	expression.value = title;
	form.onclick = evt => { evt.stopPropagation() };
	btnAdd.onclick = function(evt) {
		let type = document.querySelector('form#add-rule-form input[name="rule-type"]:checked');
		if (type == null) {
			console.warn('Selected rule not found');
			return false;
		}
		let value = expression.value.trim();
		switch (type.value) {
			case 'plaintext':
				if (!value || ignoreRules.includes(value)) break;
				ignoreRules.push(value);
				GM_setValue('ignore_rules', ignoreRules);
				break;
			case 'regexp':
				try { new RegExp(value, 'i') } catch(e) {
					alert('RegExp syntax error: ' + e);
					return false;
				}
				if (!value) break;
				value = '/' + value + '/';
				if (chkCaseless.checked) value += 'i';
				if (ignoreRules.includes(value)) break;
				ignoreRules.push(value);
				GM_setValue('ignore_rules', ignoreRules);
				break;
			default:
				console.warn('Invalid rule type value:', type);
				return false;
		}
		modal.remove();
	};
	modal.onclick = btnCancel.onclick = evt => { modal.remove() };
	//setTimeout(() => { modal.style.opacity = 1 });
	Promise.resolve(modal).then(elem => { elem.style.opacity = 1 });

	function enableElements(...n) {
		n.forEach(function(n) {
			let radio = document.querySelector('div#add-rule input[type="radio"][value="' + n + '"]');
			if (radio == null) return;
			radio.parentNode.style.opacity = 0.5;
			radio.disabled = true;
		});
	}
}

function loadArticles(elem = null) {
	return lastId > 0 ? new Promise(function(resolve, reject) {
		if (elem instanceof HTMLElement) {
			elem.style.padding = '3px 9px';
			elem.style.color = 'white';
			elem.style.backgroundColor = 'red';
			elem.textContent = 'Scanning...';
		}
		let articles = [ ];

		function loadPage(page) {
			let url = document.location.origin + '/portal.php?mode=articles&block=aapp';
			if (page > 0) url += '&start=' + (page - 1) * 8;
			if (elem instanceof HTMLElement) elem.textContent = 'Scanning...page ' + (page || 1);
			localXHR(url).then(function(document) {
				articlesIter:
				for (let table of document.body.querySelectorAll('div#wrapcentre > table > tbody > tr > td:last-of-type > table')) {
					let articleId = table.querySelector('td.postbody > a');
					articleId = articleId != null ? parseInt(new URLSearchParams(articleId.search).get('t')) : undefined;
					console.assert(articleId > 0, 'articleId > 0', table);
					if (!articleId) return; else if (articleId <= lastId) {
						if (elem instanceof HTMLElement) {
							elem.style.backgroundColor = 'green';
							elem.textContent = articles.length > 0 ? 'Showing ' + articles.length.toString() + ' unread articles'
								: 'No new articles found';
						}
						resolve(articles);
						return;
					}
					for (var tr of table.querySelectorAll('tbody > tr:not(:first-of-type)')) tr.remove();
					function cleanElement(elem) {
						if (elem instanceof Node) for (let child of elem.childNodes)
							if (child.nodeType == Node.TEXT_NODE && !child.textContent.trim()) elem.removeChild(child);
					}
					tr = table.querySelector('tbody > tr:first-of-type');
					let a, title;
					if ((th = table.querySelector('th[align="left"]')) != null) {
						title = th.textContent.trim();
						for (let expr of ignoreRules) {
							let rx = /^\/(.+)\/([dgimsuy]*)$/.exec(expr);
							if (rx != null) try {
								if (new RegExp(rx[1], rx[2]).test(title)) continue articlesIter;
							} catch(e) {
								console.warn(e);
								continue;
							} else if (title.toLowerCase().includes(expr.toLowerCase())) continue articlesIter;
						}
						a = document.createElement('a');
						a.setAttribute('articleId', articleId);
						a.href = './viewtopic.php?t=' + articleId;
						a.target = '_blank';
						a.textContent = title;
						a.style = 'color: white !important; cursor: pointer;';
						while (th.firstChild != null) th.removeChild(th.firstChild);
						th.append(a);
					}
					if ((th = table.querySelector('th[align="center"] > a')) != null)
						th.style ='color: silver !important;';
					if ((th = table.querySelector('th[align="right"] > strong')) != null) {
						th.style = 'color: burlywood !important;';
						th.parentNode.style = 'color: silver !important;';
					}
					if (tr != null) {
						th = document.createElement('th');
						th.width = '2em';
						th.align = 'right';
						a = document.createElement('a');
						a.textContent = '[X]';
						a.href = '#';
						a.onclick = function(evt) {
							addFilter([
								/\s+v\d+\.\d+\b.*$/,
								/(?:\s+(\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))+\s*$/,
							].reduce((acc, rx) => acc.replace(rx, ''), title));
							return false;
						};
						th.append(a);
						tr.append(th);
					}
					for (var th of table.querySelectorAll('tbody > tr > th')) {
						th.style.backgroundImage = 'none';
						th.style.backgroundColor = 'darkslategray';
						cleanElement(th);
					}
					cleanElement(table);
					articles.push(table);
				}
				loadPage((page || 1) + 1);
			}).catch(reject);
		}

		return loadPage();
	}) : Promise.reject('There\'s no last read mark');
}

function listUnread(elem = null) {
	if (!(lastId > 0)) {
		alert('You need to have previously marked all articles read to have checkpoint to stop scanning');
		return false;
	}
	let td = document.body.querySelector('div#wrapcentre > table > tbody > tr > td:last-of-type'), table;
	if (td == null) throw 'Invalid page structure';
	while (td.firstChild != null) td.removeChild(td.firstChild);
	while ((table = document.body.querySelector('div#wrapcentre > table[width="100%"]:nth-of-type(2)')) != null
		&& table.querySelector('p.breadcrumbs, p.datetime') == null) table.remove();
	loadArticles(elem).then(articles => { articles.forEach(article => td.append(article)) });
}

function markAllRead(elem = null) {
	function scanPage(document) {
		console.assert(document instanceof HTMLDocument);
		GM_setValue('latest_read', Math.max(...Array.from(document.body.querySelectorAll('div#wrapcentre > table > tbody > tr > td:last-of-type > table')).map(function(table) {
			let a = table.querySelector('td.postbody > a') || table.querySelector('th[align="left"] > a[articleId]');
			return a != null ? parseInt(new URLSearchParams(a.search).get('t')) : undefined;
		}).filter(id => id > 0)));
		if (elem != null) {
			elem.style.padding = '3px 9px';
			elem.style.color = 'white';
			elem.style.backgroundColor = 'green';
			elem.textContent = 'All releases marked as read, reloading page...';
		}
		window.document.location.reload();
	}

	// (!onfirm('Are yuo sure to mark everything read?')) return;
	if (filtered) scanPage(document);
		else localXHR(document.location.origin + '/portal.php?mode=articles&block=aapp').then(scanPage);
}

GM_registerMenuCommand('Show unread posts in compact view', listUnread, 'S');
GM_registerMenuCommand('Mark everything read', markAllRead, 'r');
for (let elem of document.querySelectorAll('div#wrapcentre > table:first-of-type > tbody > tr > td:first-of-type > div > iframe'))
	elem.parentNode.parentNode.removeChild(elem.parentNode);
let td = document.body.querySelector('div#menubar > table > tbody > tr:first-of-type > td[class^="row"]');
if (td != null) {
	let p = document.createElement('p');
	p.className = 'breadcrumbs';
	p.style = 'margin-right: 3em; float: right;';
	let a = document.createElement('a');
	a.textContent = 'Mark all releases read';
	a.href = '#';
	a.id = 'mark-all-read';
	a.onclick = function(evt) {
		markAllRead(evt.currentTarget);
		return false;
	};
	p.append(a);
	td.append(p);
	p = document.createElement('p');
	p.className = 'breadcrumbs';
	p.style = 'margin-right: 3em; float: right;';
	a = document.createElement('a');
	a.textContent = 'List only new releases';
	a.href = '#';
	a.id = 'list-only-new';
	a.onclick = function(evt) {
		listUnread(evt.currentTarget);
		return false;
	};
	p.append(a);
	td.append(p);
}
for (let tr of document.querySelectorAll('div#wrapcentre > table > tbody > tr > td > table > tbody > tr[class^="row"]')) {
	let id = tr.querySelector('td.postbody > a');
	if (id != null) id = parseInt(new URLSearchParams(id.search).get('t')); else return;
	if (id <= lastId) tr.style.backgroundColor = '#dcd5c1';
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址