North-Plus Notification

查看自己发的主题的回复

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         North-Plus Notification
// @namespace    https://github.com/sssssssl/NP-scripts
// @version      0.1
// @description  查看自己发的主题的回复
// @author       sl
// @match        https://*.white-plus.net/*
// @match        https://*.south-plus.net/*
// @match        https://*.imoutolove.me/*
// @require      https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
	'use strict';

	// 1. CONSTANTS

	var lastPullTimeKey = 'lastPullTime';
	var unACKTimeKey = 'lastUnACKTime';
	var unACKDataKey = 'unACKData';
	var mappingKey = 'tidCommentMap';
	var uidKey = 'uid';
	// this needs to be tested.
	var pullInterval = 1000 * 60 * 2;
	var maxUnACKAge = 1000 * 60 * 1;
	var checkInterval = 1000 * 5 * 1;
	var maxRetry = 5;

	// 2. GLOBAL STATE

	var logLevels = {
		verbose: 1,
		silent: 2
	};

	var dev = false;

	var g = {
		url: window.location.href,
		stopNofitication: false,
		unInitialized: true,
		stopLoop: false,
		retry: 0,
		has_mypost_btn: false,
		checkInterval: checkInterval,
		pullInterval: pullInterval,
		logLevel : logLevels.verbose
	};

	var originalTitle = document.title;

	// 3. CODE ENTRYPOINT

	var lastPullTime = GM_getValue(lastPullTimeKey);
	// 第一次在用户机器上运行
	if (!lastPullTime) {
		getPostStatus(true);
	}

	grabUserInfo();
	addStopBlinkCallback();
	addSelfCommentCallback();
	mainLoop();

	function mainLoop() {
		app();
		setTimeout(function () {
			if (!g.stopLoop) {
				mainLoop();
			}
		}, g.checkInterval);
	}

	function app() {
		var unACKData = GM_getValue(unACKDataKey);
		if (unACKData) {
			log(['存在未合并数据', unACKData]);

			g.stopNotification = false;
			sendNotification(unACKData);

			// var lastUnACKTime = GM_getValue(unACKTimeKey);
			// var now = Date.now()
			// // 当未确认数据寿命达到允许的最大值,进行合并
			// if (lastUnACKTime && (now - lastUnACKTime) > maxUnACKAge) {
			// 	updateMapping(unACKData);
			// }
		}

		getPostStatus(false);
	}

	function getPostStatus(first = false) {

		function timeCheck() {
			var now = Date.now();
			var lastPullTime = GM_getValue(lastPullTimeKey);
			if (lastPullTime) {
				var dtime = (now - lastPullTime);
				log('距离上次拉取评论 ' + (dtime / 1000) + ' 秒');
			}
			return !(lastPullTime && dtime < g.pullInterval);
		}

		var needUpdate = timeCheck();

		if (!needUpdate) {
			g.checkInterval = g.checkInterval + 1000 * 5;
			g.checkInterval = Math.min(g.checkInterval, 1000 * 20);
			g.pullInterval = g.pullInterval + 1000 * 60 * 1;
			if (g.pullInterval > 1000 * 60 * 4) {
				g.pullInterval = pullInterval;
			}
			return;
		}
		else {
			g.checkInterval = checkInterval;
			g.pullInterval = pullInterval;
		}

		if (needUpdate) {

			var myPostURL = getPostURL(dev);

			$.ajax({
				url: myPostURL,
				type: 'GET',
				success: function (data) {

					// 调用返回后才更新 lastPullTime, 增加访问服务器的时间间隔
					GM_setValue(lastPullTimeKey, Date.now());

					var newMap = getParser(dev)(data);
					var mapping = getMapping();
					var diffInfo = compareMap(mapping, newMap);
					if (diffInfo.nNewComment > 0) {
						if (first) {
							log('首次执行.');
							updateMapping(diffInfo.diffMap);
						}
						else {
							log(['写入 unACKData:', diffInfo]);
							GM_setValue(unACKDataKey, diffInfo);
							GM_setValue(unACKTimeKey, Date.now());
							sendNotification(diffInfo);
						}
					}
					else {
						log('评论数量没有变化.');
					}
				},
				error: function (data) {
					g.retry += 1;
					if (g.retry == maxRetry) {
						g.stopLoop = true;
						console.log('网络似乎有问题,停止拉取评论');
					}
					console.log([data]);
				}
			});

		}
	}


	// 4. DATA PROCESSING

	function getPostURL(lv) {
		if (lv) {
			return 'posts';
		}
		return 'u.php?action-topic.html';
	}

	function getParser(lv) {
		if (lv) {
			return JSON.parse;
		}
		return parseData;
	}

	// 比较拉取到的评论数与本地保存的评论数的区别
	function parseData(data) {
		var dataMap = {};
		var pat = /read.php\?tid-(\d+).html.+?回复:(\d+)/g;
		var m;
		while (m = pat.exec(data)) {
			var tid = m[1];
			var num = parseInt(m[2]);
			dataMap[tid] = num;
		}
		return dataMap;
	}

	function compareMap(baseMap, newMap) {
		var diffInfo = {
			nNewComment: 0,
			diffMap: {},
		};
		for (var key in newMap) {
			diffInfo.diffMap[key] = newMap[key];
			if (!(key in baseMap)) {
				diffInfo.nNewComment += newMap[key];
			}
			else {
				diffInfo.nNewComment += newMap[key] - baseMap[key];
			}
		}
		return diffInfo;
		// debug
		if (diffInfo.nNewComment > 0) {
			log([
				'from compareMapp:',
				'有 ' + diffInfo.nNewComment + ' 条新评论.',
				diffInfo.diffMap,
			]);
		}
	}

	function mergeMap(baseMap, diffMap) {
		for (var key in diffMap) {
			baseMap[key] = diffMap[key];
		}
	}

	function getMapping() {
		var mapping = GM_getValue(mappingKey);
		if (!mapping) {
			mapping = {};
		}
		return mapping;
	}

	function updateMapping(diffMap) {
		var mapping = getMapping();
		mergeMap(mapping, diffMap);
		GM_setValue(mappingKey, mapping);
		GM_setValue(unACKDataKey, null);
		GM_setValue(unACKTimeKey, null);
		log([
			'from updateMapping',
			'写入 mapping:',
			mapping,
		]);
	}

	function grabUserInfo() {
		var uid = GM_getValue(uidKey);
		if (uid) {
			g.uid = uid;
			return;
		}
		log('try to get uid from page...');
		var userInfo = $('span.user-infoWraptwo');
		if (!userInfo) {
			log('no userinfo span, unable to get uid.');
			return;
		}
		var userData = $(userInfo).text();
		var uidPat = /UID:\s(\d+)/;
		var m = uidPat.exec(userData);
		if (m && m.length) {
			uid = m[1];
			log(`got uid: ${uid}.`);
			g.uid = uid;
			GM_setValue(uidKey, uid);
		}
	}

	// 自己发的评论不会导致回复数增加
	function addSelfCommentCallback() {
		var form = $('form[name=FORM]');
		if (!form) {
			log('no form, no worries.');
			return;
		}
		var modifyPat = /post.php\?action-modify/;
		// 编辑回复页面,不增加回复数
		if (modifyPat.test(g.url)) {
			log('modify page...');
			return;
		}
		$(form).on('submit', function () {
			var tid = $('form > input[name=tid]').attr('value');
			log(`tid: ${tid}`);
			if (!tid) {
				log('发帖页面,不是评论,无 tid');
				return;
			}
			// 评论
			var mapping = GM_getValue(mappingKey);
			if (!mapping) {
				mapping = {};
			}
			// 可能是自己刚刚发的帖子,也可能是别人的帖子
			// 给刚刚发的帖子评论就无视掉好了
			if (!(tid in mapping)) {
				log(`对 tid=${tid} 的帖子评论.`);
			}
			else {
				mapping[tid] += 1;
				GM_setValue(mappingKey, mapping);
				log('回复自己的帖子.')
			}
		});
	}


	// 5. UI

	// 给【我的主题】按钮添加停止闪烁的回调函数,每个页面只要做一次就行了
	function addStopBlinkCallback() {
		var myPostButton = $('#infobox').find('.link5')[0];
		// 有些页面没有这个按钮,不需要更新 UI。
		if (!myPostButton) {
			g.has_mypost_btn = false;
			return;
		}
		else {
			g.has_mypost_btn = true;
		}
		$(myPostButton).wrap('<span></span>');
		var span = $(myPostButton).parent()[0];
		addSpinEffect();
		// enforce event capture, disable event bubbling.
		span.addEventListener('click', function (e) {
			var unACKData = GM_getValue(unACKDataKey);
			if (unACKData) {
				updateMapping(unACKData.diffMap);
			}
			g.stopNofitication = true;
		}, true);
	}

	function addSpinEffect() {
		$('head').append('<style>#btn-my-post {' +
			'display: inline-block; font-weight:bold;' +
			'animation:myfirst 1s;animation-iteration-count:infinite;' +
			'-webkit-animation: myfirst 1s;-webkit-animation-iteration-count: infinite;}' +
			'@keyframes myfirst {' +
			'0% { transform: rotate(0deg) scale(1, 1);; }' +
			'25% { transform: rotate(15deg) scale(1.2, 1.2);; }' +
			'50% { transform: rotate(-15deg) scale(1.2, 1.2);; }' +
			'100% { transform: rotate(0deg) scale(1, 1);; }}' +
			'@-webkit-keyframes myfirst {' +
			'0% { transform: rotate(0deg) scale(1, 1);; }' +
			'25% { transform: rotate(15deg) scale(1.2, 1.2);; }' +
			'50% { transform: rotate(-15deg) scale(1.2, 1.2);; }' +
			'100% { transform: rotate(0deg) scale(1, 1);; }}' +
			'#btn-my-post > a {color:#FF0000}' +
			'</style>'
		);
	}

	function sendNotification(diffInfo) {

		if (!g.has_mypost_btn) {
			return;
		}

		log([
			'from sendNotification',
			'stopNofitication:' + g.stopNofitication,
			'unInitialized: ' + g.unInitialized,
		]);

		var nNewComment = diffInfo.nNewComment;
		if (!nNewComment) {
			return;
		}
		else if(g.nNewCommentCache && g.nNewCommentCache == nNewComment) {
			return;
		}
		else {
			g.nNewCommentCache = nNewComment;
		}

		var title_blk = `【新回复 x ${nNewComment}】${originalTitle}`;

		var title_list = [originalTitle, title_blk];
		var fw_list = ['normal', 'bold'];

		g.style = {
			title_list : title_list,
			fw_list : fw_list,
			index : 0
		};

		var myPostButton = $('#infobox').find('.link5')[0];
		$(myPostButton).text(`我的主题( ${nNewComment} 条新回复)`);

		function updateStyle(index) {
			document.title = g.style.title_list[index];
		}


		function blink() {
			setTimeout(function () {
				g.style.index = 1 - g.style.index;
				updateStyle(g.style.index);
				if (g.stopNofitication) {
					updateStyle(0);
					$('#btn-my-post').attr('id', '');
				}
				else {
					blink();
				}
			}, 1000);
		};

		if (g.unInitialized) {

			g.unInitialized = false;
			blink();

			var span = $(myPostButton).parent()[0];
			$(span).attr('id', 'btn-my-post');
		}
	}

	function log(sl, lv = g.logLevel) {
		if (lv == logLevels.verbose) {
			if (sl.constructor === Array) {
				sl.forEach(function (s) {
					console.log(s);
				});
			}
			else {
				console.log(sl);
			}
		}
	}

})();