// ==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.41 // @lastchanges multiple urls for settings // @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 // @downloadURL none // ==/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 . // // 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', '.*\.?diestandard\.at', '.*\.?dastandard\.at' ], 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) { for (var j = 0; j < theSettings[i].url.length; ++j) { var myRegExp = new RegExp(theSettings[i].url[j], '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(); //})();