NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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.4192 // @lastchanges workaround for bug in GreaseMonkey 3.2 // @license GNU GPL version 3 // @released 2014-02-20 // @updated 2014-06-10 // @homepageURL https://github.com/theoky/HistoryOfTheSeen // // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_listValues // @grant GM_addStyle // // for testing purposes (set FireFox 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/* // @include http*://www.nytimes.com/* // @require http://code.jquery.com/jquery-2.1.1.min.js // @require http://code.jquery.com/ui/1.11.2/jquery-ui.js // @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) 2015 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 34 and GreaseMonkey 2.3 //------------------------------------------------- //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.85, 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, considerViewPort: true, dbOpsPerRun: 5 }; var UNDEF = 'undefined'; var DEFAULT_TAG = 'a'; var defaultGetContentFct = function(elem) { if ((typeof elem != UNDEF) && (typeof elem.href != UNDEF)) { return elem.href; } return UNDEF; }; var AFTER_SCROLL_DELAY = 750; var DO_DEBUG = false; var DEBUG_LVL_ERROR = 1; var DEBUG_LVL_WARN = 2; var DEBUG_LVL_INFO = 4; var perUrlSettings = [ { url : ['.*\.?slashdot\.org' ], tag : 'article', upTrigger: "../article", getContent: function(elem) { if ((typeof elem != UNDEF) && (typeof elem.id != UNDEF)) { return elem.id; } return UNDEF; }, parentHints : [ ] }, { 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 ')]" ] }, { url : ['nytimes\.com'], upTrigger: "../a", parentHints : [ // "ancestor::li[contains(concat(' ', @class, ' '), ' portal-post ')]", "ancestor::div[contains(concat(' ', @class, ' '), ' collection ')]" ] } ]; var dimMap = {}; var countDownTimer = defaultSettings.steps; var theHRefs = null; var curSettings = null; var KEY_LAST_EXPIRE_OP = "lastExpire"; var timeOutAfterLastScroll = UNDEF; var tag2Process = null; var getContentFct = null; var theDomain = null; var progressbar; var progressLabel; // Styling var progressBarStyle = ".ui-widget {" + " font-family: Verdana,Arial,sans-serif !important;" + " font-size: 1.1em !important;" + "}" + ".ui-widget-content {" + " border: 1px solid #aaaaaa !important;" + " color: #222222 !important;" + "}" + ".ui-widget-header {" + " border: 1px solid #aaaaaa !important;" + " background: #cccccc !important;" + " color: #222222 !important;" + " font-weight: bold !important;" + "}" + ".ui-progressbar {" + " height: 2em !important;" + " text-align: left !important;" + " overflow: hidden !important;" + "}" + ".ui-progressbar .ui-progressbar-value {" + " margin: -1px !important;" + " height: 100% !important;" + "}" + ".ui-progressbar .ui-progressbar-overlay {" + " background: url('data:image/gif;base64,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') !important;" + " height: 100% !important;" + " filter: alpha(opacity=25) !important; /* support: IE8 */" + " opacity: 0.25 !important;" + "}" + // ".ui-progressbar-indeterminate .ui-progressbar-value {" + // " background-image: none !important;" + // "}" + ".ui-progressbar {" + " height: 2em !important;" + " text-align: left !important;" + " overflow: hidden !important;" + " position: absolute !important;" + " left: 20% !important;" + " top: 4px !important;" + " width: 60% !important;" + " z-index: 255 !important;" + "}" + ".progress-label {" + " position: absolute !important;" + " left: 5% !important;" + " top: 4px !important;" + " font-weight: bold !important;" + " text-shadow: 1px 1px 0 #fff !important;" + " z-index: 256 !important;" + "}"; // Debugging function debuglog(msg) { if (DO_DEBUG) { console.log(msg); } } function debugLogLvl(lvl, msg) { if (lvl & DEBUG_LVL_ERROR) { console.log("error: " + msg); } if (DO_DEBUG) { if (lvl & DEBUG_LVL_WARN) { console.log("warn:" + msg); } if (lvl & DEBUG_LVL_INFO) { console.log(msg); } } } var g_index; var g_keys; var g_lengthOfKeysArray; var g_workInProgress = false; var g_par1 = UNDEF; var g_par2 = UNDEF; var g_workerFctDefault = function(key, par1, par2) { GM_deleteValue(key); }; var g_workerFct = g_workerFctDefault; var g_finishFct_Default = function() { document.location.reload(true); }; var g_finishFct = g_finishFct_Default; var g_label; function appendProgressBar() { $("body").append ( '\ <div id="progressbar" class="ui-progressbar ui-progressbar-indeterminate"><div class="progress-label">History of the Seen: Resetting DB for current domain...</div></div>'); } function removeProgressBar(reload) { $("#progressbar").remove(); if (reload) { document.location.reload(true); } } /* * Init function for "threading" */ function initThreadingLoop() { if (g_workInProgress) { debugLogLvl(DEBUG_LVL_ERROR, "initThreading with already threading in progress."); return; } g_workInProgress = true; g_index = 0; g_keys = GM_listValues(); if (!g_keys) { debugLogLvl(DEBUG_LVL_WARN, "g_keys empty?"); return; } g_lengthOfKeysArray = g_keys.length; appendProgressBar(); progressbar = $("#progressbar"); progressLabel = $(".progress-label"); progressbar.progressbar({ value : false, change : function() { progressLabel.text(g_label + progressbar.progressbar("value").toFixed(2) + "% "); }, complete : function() { progressLabel.text(" History of the Seen: Operation Complete! "); } }); progressbar.progressbar("value", 0); setTimeout(doThreadWork, 1); } /* * Worker method */ function doThreadWork() { if (!g_workInProgress) { return; } var i = 0; var currentKey = null; currentKey = g_keys[g_index]; while (i < defaultSettings.dbOpsPerRun && currentKey) { g_workerFct(currentKey, g_par1, g_par2); g_index ++; i++; currentKey = g_keys[g_index]; } progressbar.progressbar("value", g_index * 100 / g_lengthOfKeysArray); if (currentKey) { setTimeout(doThreadWork, 10); } else { removeProgressBar(false); if (g_finishFct !== UNDEF) { g_finishFct(); } g_workInProgress = false; } } // Resetting section function resetAllUrls() { if (!g_workInProgress && confirm('Are you sure you want to erase the complete seen history?')) { g_label = " History of the Seen: Cleaning DB, done "; g_par1 = UNDEF; g_par2 = UNDEF; g_workerFct = g_workerFctDefault; g_finishFct = g_finishFct_Default; initThreadingLoop(); } } function resetUrlsForCurrentHelper(dKey, domainOrUri) { if (confirm('Are you sure you want to erase the seen history for ' + domainOrUri + '?')) { g_label = " History of the Seen: Cleaning DB, done "; g_par1 = dKey; g_par2 = domainOrUri; g_workerFct = function(key, dKey, domainOrUri) { if (key == KEY_LAST_EXPIRE_OP){ return; } try { var val = GM_getValue(key, "{}"); var dict = JSON.parse(val); if(dict) { if (dict[dKey] == domainOrUri) { GM_deleteValue(key); } } } catch (e) { console.log(e); } }; g_finishFct = g_finishFct_Default; initThreadingLoop(); } } function resetUrlsForCurrentDomain() { resetUrlsForCurrentHelper("domain", document.domain); } function resetUrlsForCurrentSite() { resetUrlsForCurrentHelper("base", document.baseURI); } function expireUrls() { debugLogLvl(DEBUG_LVL_INFO, "expireUrls"); if (defaultSettings.cleanOnlyDaily) { var lastExpireDate = new Date(GM_getValue(KEY_LAST_EXPIRE_OP, nDaysOlderFromNow(2))); var diff = Math.abs((new Date()) - lastExpireDate); if (diff / 1000 / 3600 / 24 < 1) { // less than one day -> no DB cleaning return; } } /* var val = GM_getValue(KEY_EXPIRE_OP_INPROGRESS); if (typeof val !== UNDEF) { // expire in progress return; } GM_setValue(KEY_EXPIRE_OP_INPROGRESS, True); */ // cutOffDate g_label = " History of the Seen: Expiring old URLs for this site, done "; g_par1 = nDaysOlderFromNow(defaultSettings.ageOfUrl); debuglog("cutOffDate" + g_par1); g_par2 = UNDEF; g_workerFct = function(key, cutOffDate, par2) { if (key == KEY_LAST_EXPIRE_OP){ return; } var dict = JSON.parse(GM_getValue(key, "{}")); if(dict) { try { debuglog(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. Probably best to completely clean DB.'); } }; g_finishFct = function() { GM_setValue(KEY_LAST_EXPIRE_OP, new Date()); } initThreadingLoop(); } function nDaysOlderFromNow(age, aDate, zeroHour) { var aDate = typeof aDate !== UNDEF ? aDate : new Date(); var zeroHour = typeof zeroHour !== UNDEF ? 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) { debugLogLvl(DEBUG_LVL_INFO, "findPerUrlSettings"); 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 (var 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 false; } var res = null; if (curSettings.upTrigger !== "") { res = document.evaluate(curSettings.upTrigger, aRoot, null, 9, null).singleNodeValue; } return res !== null; } /* * Set the opacity for specified links */ function dimLinks() { var interval = (1 - defaultSettings.targetOpacity4Dim)/defaultSettings.steps; var countDownTimer = countDownTimer - 1; var curOpacity = defaultSettings.targetOpacity4Dim + interval*countDownTimer; // TODO: Better iterate over dimmap 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) { var to = setTimeout(dimLinks, defaultSettings.dimInterval); } } /* * Check if an element is fully drawn on the viewport * from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling?lq=1 */ function isFullyInView(elem) { debugLogLvl(DEBUG_LVL_INFO, "isFullyInView"); var docViewTop = $(window).scrollTop(); var docViewBottom = docViewTop + $(window).height(); var elemTop = $(elem).offset().top; var elemBottom = elemTop + $(elem).height(); return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); // is really fully in view // return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) // && (elemBottom <= docViewBottom) && (elemTop >= docViewTop) ); } /* * Called after scrolling finished for defined time */ function evaluateElems() { debugLogLvl(DEBUG_LVL_INFO, "evaluate all"); processElements(false); timeOutAfterLastScroll = UNDEF; } /* * Wait for scrolling to end */ function onScroll() { if (timeOutAfterLastScroll !== UNDEF) { window.clearTimeout(timeOutAfterLastScroll) } timeOutAfterLastScroll = setTimeout(evaluateElems, AFTER_SCROLL_DELAY); } /* * Process all elements */ function processElements(firstCall) { debugLogLvl(DEBUG_LVL_INFO, "processElements"); var allTagElems = document.getElementsByTagName(tag2Process); var elemMap = {}; var theBase = document.baseURI; // Change the DOM // First loop: gather all new links and make already seen opaque. for(var i = 0; i < allTagElems.length; i++) { var hash = 'm' + hex_md5(getContentFct(allTagElems[i])); // setValue needs letter in the beginning, thus use of 'm' debugLogLvl(DEBUG_LVL_INFO, "hash: " + hash); var key = GM_getValue(hash); if (typeof key !== UNDEF && key !== null) { // workaround for issue https://github.com/greasemonkey/greasemonkey/issues/2156 // key found -> loaded this reference already debugLogLvl(DEBUG_LVL_INFO, "key found"); if (firstCall) { var done = false; if(goUp(curSettings, allTagElems[i])) { var pe = locateParentElem(curSettings, theDomain, allTagElems[i]) // console.log("locate parent done", pe); if (pe) { pe.style.opacity = defaultSettings.targetOpacity; done = true; } } if (!done) { // change display allTagElems[i].style.opacity = defaultSettings.targetOpacity; debugLogLvl(DEBUG_LVL_INFO, "changing opacity"); } } } else { //check if element is fully visible debugLogLvl(DEBUG_LVL_INFO, "key not found"); if (isFullyInView(allTagElems[i])) { debuglog(allTagElems[i] + " is in view"); // key not found, store it with current date elemMap[hash] = {"domain":theDomain, "date":(new Date()).getTime(), "base":theBase}; dimMap[hash] = allTagElems[i]; } } } // remember all new urls to hide the next time for (var e2 in elemMap) { GM_setValue(e2, JSON.stringify(elemMap[e2])); } theHRefs = allTagElems; if (firstCall) { var to = setTimeout(dimLinks, defaultSettings.dimInterval); } } // 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); GM_addStyle(progressBarStyle); // Main part function run_script() { debugLogLvl(DEBUG_LVL_INFO, "run"); dimMap = {}; theDomain = document.domain; curSettings = findPerUrlSettings(perUrlSettings, theDomain); tag2Process = DEFAULT_TAG; getContentFct = defaultGetContentFct; if (typeof curSettings != UNDEF) { if (typeof curSettings.tag != UNDEF) { tag2Process = curSettings.tag; } if (typeof curSettings.getContent != UNDEF) { getContentFct = curSettings.getContent; } } expireUrls(); processElements(true); window.addEventListener("scroll", onScroll, false); } run_script(); })();