Greasy Fork

History of the Seen

Script to implement a history of the seen approach for some news sites. Details at https://github.com/theoky/HistoryOfTheSeen

目前为 2014-08-30 提交的版本。查看 最新版本

// ==UserScript==
// @name History of the Seen
// @namespace https://github.com/theoky/HistoryOfTheSeen
// @description Script to implement a history of the seen approach for some news sites. Details at https://github.com/theoky/HistoryOfTheSeen
// @author          Theoky
// @version	        0.40
// @lastchanges     semicolons, upTriggers, clean DB only daily
// @license         GNU GPL version 3
// @released        2014-02-20
// @updated         2014-08-31
// @homepageURL   	https://github.com/theoky/HistoryOfTheSeen
//
// @grant      GM_getValue
// @grant      GM_setValue
// @grant      GM_deleteValue
// @grant      GM_registerMenuCommand
// @grant      GM_listValues
//
// for testing (set greasemonkey.fileIsGreaseable) 
// @include file://*testhistory.html
//
// @include http*://*.derstandard.at/*
// @include http*://*.faz.net/*
// @include http*://*.golem.de/*
// @include http*://*.handelsblatt.com/*
// @include http*://*.heise.de/newsticker/*
// @include http*://*.kleinezeitung.at/*
// @include http*://*.nachrichten.at/*
// @include http*://*.oe24.at/*
// @include http*://*.orf.at/*
// @include http*://orf.at/*
// @include http*://*.reddit.com/*
// @include http*://*.spiegel.de/*
// @include http*://*.sueddeutsche.de/*
// @include http*://*.welt.de/*
// @include http*://*.wirtschaftsblatt.at/*
// @include http*://*.zeit.de/*
// @include http*://dastandard.at/*
// @include http*://derstandard.at/*
// @include http*://diepresse.com/*
// @include http*://diestandard.at/*
// @include http*://kurier.at/*
// @include http*://slashdot.org/*
// @include http*://taz.de/*
// @include http*://notalwaysright.com/

// @require https://greasyfork.org/scripts/130-portable-md5-function/code/Portable%20MD5%20Function.js?version=10066
// was require md5.js 
// was require http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js
// ==/UserScript==

// Copyright (C) 2014  T. Kopetzky - theoky
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// Tested with Firefox 31 and GreaseMonkey 2.1

//-------------------------------------------------
//Functions

//(function(){

	var defaultSettings = {
			ageOfUrl: 5, 	// age in days after a url is deleted from the store
							// < 0 erases all dates (disables history)
			targetOpacity: 0.3,
			targetOpacity4Dim: 0.65,
			steps: 10,
			dimInterval: 30000,
			expireAllDomains: true,		// On fast machines this can be true and expires
							// all domains in the database with each call. If false,
							// only the urls of the current domain are expired which 
							// is slightly faster.
			cleanOnlyDaily: true
		}

	var perUrlSettings = [
		{
			url : '.*\.?derstandard\.at',
			// TODO: der, die, das standard
			upTrigger: "../a",
			parentHints : [
					"ancestor::div[contains(concat(' ', @class, ' '), ' text ')]",
					"ancestor::ul[@class='stories']" ]
		},

		{
			url : 'notalwaysright\.com',
			upTrigger: "../a[@rel='bookmark']",
			parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' post ')]" ]
		},

		{
			url : '.*\.?golem.de',
			upTrigger: "../a",
			parentHints : [ "ancestor::li",
					"ancestor::section[@id='index-promo']",
					"ancestor::section[contains(concat(' ', @class, ' '), ' promo ')]" ]
		},

		{
			url : '.*\.?reddit.com',
			// class="title may-blank  srTagged imgScanned"
			upTrigger: "../a[contains(@class, 'title') and contains(@class, 'may-blank')]",
			parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' thing ')]" ]
		}
	]
	
	var dimMap = {};
	var countDownTimer = defaultSettings.steps;
	var theHRefs = null;
	var curSettings = null;
	var keyLastExpireOp = "lastExpire";

	function resetAllUrls() {
		if (confirm('Are you sure you want to erase the complete seen history?')) {
			var keys = GM_listValues();
			for (var i=0, key=null; key=keys[i]; i++) {
				GM_deleteValue(key);
			}
			document.location.reload(true);
		}
	}

	function resetUrlsForCurrentHelper(dKey, domainOrUri) {
		if (confirm('Are you sure you want to erase the seen history for ' + 
				domainOrUri + '?')) {
			var keys = GM_listValues();
			for (var i=0, key=null; key=keys[i]; i++) {
				var dict = JSON.parse(GM_getValue(key, "{}"));
				if(dict) {
					if (dict[dKey] == domainOrUri) {
						GM_deleteValue(key);
					}
				}
			}
			document.location.reload(true);
		}
	}
	
	function resetUrlsForCurrentDomain() {
		resetUrlsForCurrentHelper("domain", document.domain)
	}
	
	function resetUrlsForCurrentSite() {
		resetUrlsForCurrentHelper("base", document.baseURI)
	}

	function expireUrls()	{
		if (defaultSettings.cleanOnlyDaily) {
			lastExpireDate = new Date(GM_getValue(keyLastExpireOp, nDaysOlderFromNow(2)));
			diff = Math.abs((new Date()) - lastExpireDate);
			if (diff / 1000 / 3600 / 24 < 1) {
				// less than one day -> no DB cleaning
				return;
			}
		}

		var keys = GM_listValues();
		if (!keys) {
			return;
		}

		var cutOffDate = nDaysOlderFromNow(defaultSettings.ageOfUrl);

		for (var i=0, key=null; key=keys[i]; i++) {
			if (key == keyLastExpireOp){
				continue;
			}
				
			var dict = JSON.parse(GM_getValue(key, "{}"));
			
			if(dict) {
				try {
					// console.log(dict["domain"], cutOffDate.getTime(), dict["date"]);
					if (cutOffDate.getTime() > dict["date"]) {
						if (defaultSettings.expireAllDomains ||
							(dict["domain"] == document.domain))
						{
							GM_deleteValue(key);
						}
					}
				} catch (e) {
					console.log(e);
				}
			}
			else {
				console.log('Error! JSON.parse failed - dict is likely to be corrupted.');
			}
		}
		GM_setValue(keyLastExpireOp, new Date());
	}

	function nDaysOlderFromNow(age, aDate, zeroHour) {
		aDate = typeof aDate !== 'undefined' ? aDate : new Date();
		zeroHour = typeof zeroHour !== 'undefined' ? zeroHour : true;
		
		var dateStore = new Date(aDate.getTime());
		var workDate = aDate;
		if (age >= 0) {
			workDate.setDate(dateStore.getDate() - age);
			if (zeroHour) {
				workDate.setHours(0,0,0,0);
			}
		}
		return workDate;
	}

	/*
	 * Find the settings for a given URL
	 */
	function findPerUrlSettings(theSettings, aDomain) {
		for (var i=0; i < theSettings.length; ++i) {
			var myRegExp = new RegExp(theSettings[i].url, 'i');
			if (aDomain.match(myRegExp)) {
				return theSettings[i];
			}
		}
	}

	/*
	 * Find the parent element as specified in the settings.
	 */
	function locateParentElem(curSettings, aDomain, aRoot) {
		if (!curSettings) {
			return null;
		}
		// console.log("locateParentElem 1", curSettings.url);
		var res = null;
		for (xpath = 0; xpath < curSettings.parentHints.length; ++xpath) {
			// console.log("locateParentElem 2", curSettings.parentHints[xpath], aRoot);
			res = document.evaluate(curSettings.parentHints[xpath], aRoot, null, 9, null).singleNodeValue;
			if (res) {
				// console.log("locateParentElem found something");
				return res;
			}
		}
		return res;
	}
	
	/*
	 * Check if the current node qualifies for looking up the hierarchy. 
	 */
	function goUp(curSettings, aRoot) {
		if (!curSettings) {
			return null;
		}

		res = null;
		if (curSettings.upTrigger != "") {
			res = document.evaluate(curSettings.upTrigger, aRoot, null, 9, null).singleNodeValue;
		}
		return res != null
	}
	
	
	function dimLinks() {
		interval = (1 - defaultSettings.targetOpacity4Dim)/defaultSettings.steps;
		countDownTimer = countDownTimer - 1;
		curOpacity = defaultSettings.targetOpacity4Dim + interval*countDownTimer;

		for(var i = 0; i < theHRefs.length; i++)
		{
			var hash = 'm' + hex_md5(theHRefs[i].href);
			if (hash in dimMap) {
				theHRefs[i].style.opacity = curOpacity;
			}
		}
		
		if (countDownTimer > 0) {
			to = setTimeout(dimLinks, defaultSettings.dimInterval);
		}
	}

//	Main part

//	Menus
	GM_registerMenuCommand("Remove the seen history for this site.", resetUrlsForCurrentSite);
	GM_registerMenuCommand("Remove the seen history for this domain.", resetUrlsForCurrentDomain);
	GM_registerMenuCommand("Remove all seen history (for all sites)!", resetAllUrls);

	function run_script() {
		// Vars
		var allHrefs = document.getElementsByTagName("a");
		var theBase = document.baseURI;
		var theDomain = document.domain;
		var elemMap = {};
		
		dimMap = {};

		curSettings = findPerUrlSettings(perUrlSettings, theDomain);
		// console.log(curSettings);

		// expire old data
		expireUrls();

		// Change the DOM

		// First loop: gather all new links and make already seen opaque.
		for(var i = 0; i < allHrefs.length; i++)
		{
			var hash = 'm' + hex_md5(allHrefs[i].href);
			// setValue needs letter in the beginning, thus use of 'm'

			var key = GM_getValue(hash);
			// console.log(allHrefs[i].href, hash.toString());

			if (typeof key != 'undefined') {
				// key found -> loaded this reference already 
				
				done = false;
				if(goUp(curSettings, allHrefs[i])) {
					pe = locateParentElem(curSettings, theDomain, allHrefs[i])
					// console.log("locate parent done", pe);
					if (pe) {
						pe.style.opacity = defaultSettings.targetOpacity;
						done = true;
					}
				}
				if (!done) {
					// change display
					allHrefs[i].style.opacity = defaultSettings.targetOpacity;
				}
				
			} else {
				// key not found, store it with current date
				elemMap[hash] = {"domain":theDomain, "date":(new Date()).getTime(), "base":theBase};
				dimMap[hash] = allHrefs[i];
			}
		}

		// remember all new urls to hide the next time
		for (var e2 in elemMap) {
			GM_setValue(e2, JSON.stringify(elemMap[e2]));
		}
		
		theHRefs = allHrefs;
		to = setTimeout(dimLinks, defaultSettings.dimInterval);
	}

	run_script();
//})();