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 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name History of the Seen
  3. // @namespace https://github.com/theoky/HistoryOfTheSeen
  4. // @description Script to implement a history of the seen approach for some news sites. Details at https://github.com/theoky/HistoryOfTheSeen
  5. // @author Theoky
  6. // @version 0.39
  7. // @lastchanges new urls, dot notation, changed dimming, hide more elements by looking up the hierarchy
  8. // @license GNU GPL version 3
  9. // @released 2014-02-20
  10. // @updated 2014-08-30
  11. // @homepageURL https://github.com/theoky/HistoryOfTheSeen
  12. //
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_deleteValue
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_listValues
  18. //
  19. // for testing (set greasemonkey.fileIsGreaseable)
  20. // @include file://*testhistory.html
  21. //
  22. // @include http*://*.derstandard.at/*
  23. // @include http*://*.faz.net/*
  24. // @include http*://*.golem.de/*
  25. // @include http*://*.handelsblatt.com/*
  26. // @include http*://*.heise.de/newsticker/*
  27. // @include http*://*.kleinezeitung.at/*
  28. // @include http*://*.nachrichten.at/*
  29. // @include http*://*.oe24.at/*
  30. // @include http*://*.orf.at/*
  31. // @include http*://orf.at/*
  32. // @include http*://*.reddit.com/*
  33. // @include http*://*.spiegel.de/*
  34. // @include http*://*.sueddeutsche.de/*
  35. // @include http*://*.welt.de/*
  36. // @include http*://*.wirtschaftsblatt.at/*
  37. // @include http*://*.zeit.de/*
  38. // @include http*://dastandard.at/*
  39. // @include http*://derstandard.at/*
  40. // @include http*://diepresse.com/*
  41. // @include http*://diestandard.at/*
  42. // @include http*://kurier.at/*
  43. // @include http*://slashdot.org/*
  44. // @include http*://taz.de/*
  45. // @include http*://notalwaysright.com/
  46.  
  47. // @require https://greasyfork.org/scripts/130-portable-md5-function/code/Portable%20MD5%20Function.js?version=10066
  48. // was require md5.js
  49. // was require http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js
  50. // ==/UserScript==
  51.  
  52. // Copyright (C) 2014 T. Kopetzky - theoky
  53. //
  54. // This program is free software: you can redistribute it and/or modify
  55. // it under the terms of the GNU General Public License as published by
  56. // the Free Software Foundation, either version 3 of the License, or
  57. // (at your option) any later version.
  58. //
  59. // This program is distributed in the hope that it will be useful,
  60. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  61. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  62. // GNU General Public License for more details.
  63. //
  64. // You should have received a copy of the GNU General Public License
  65. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  66. //
  67. // Tested with Firefox 31 and GreaseMonkey 2.1
  68.  
  69. //-------------------------------------------------
  70. //Functions
  71.  
  72. //(function(){
  73.  
  74. var defaultSettings = {
  75. ageOfUrl: 5, // age in days after a url is deleted from the store
  76. // < 0 erases all dates (disables history)
  77. targetOpacity: 0.3,
  78. targetOpacity4Dim: 0.65,
  79. steps: 10,
  80. dimInterval: 30000,
  81. expireAllDomains: true // On fast machines this can be true and expires
  82. // all domains in the database with each call. If false,
  83. // only the urls of the current domain are expired which
  84. // is slightly faster.
  85. }
  86.  
  87. var perUrlSettings = [
  88. {
  89. url : '.*\.?derstandard\.at',
  90. // TODO: der, die, das standard
  91. upTrigger: "../a",
  92. parentHints : [
  93. "ancestor::div[contains(concat(' ', @class, ' '), ' text ')]",
  94. "ancestor::ul[@class='stories']" ]
  95. },
  96.  
  97. {
  98. url : 'notalwaysright\.com',
  99. upTrigger: "../a",
  100. parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' post ')]" ]
  101. },
  102.  
  103. {
  104. url : '.*\.?golem.de',
  105. upTrigger: "../a",
  106. parentHints : [ "ancestor::li",
  107. "ancestor::section[@id='index-promo']",
  108. "ancestor::section[contains(concat(' ', @class, ' '), ' promo ')]" ]
  109. },
  110.  
  111. {
  112. url : '.*\.?reddit.com',
  113. // class="title may-blank srTagged imgScanned"
  114. upTrigger: "../a[contains(@class, 'title') and contains(@class, 'may-blank')]",
  115. parentHints : [ "ancestor::div[contains(concat(' ', @class, ' '), ' thing ')]" ]
  116. }
  117. ]
  118. var dimMap = {};
  119. var countDownTimer = defaultSettings.steps;
  120. var theHRefs = null;
  121. var curSettings = null;
  122.  
  123. function resetAllUrls() {
  124. if (confirm('Are you sure you want to erase the complete seen history?')) {
  125. var keys = GM_listValues();
  126. for (var i=0, key=null; key=keys[i]; i++) {
  127. GM_deleteValue(key);
  128. }
  129. document.location.reload(true);
  130. }
  131. }
  132.  
  133. function resetUrlsForCurrentHelper(dKey, domainOrUri) {
  134. if (confirm('Are you sure you want to erase the seen history for ' +
  135. domainOrUri + '?')) {
  136. var keys = GM_listValues();
  137. for (var i=0, key=null; key=keys[i]; i++) {
  138. var dict = JSON.parse(GM_getValue(key, "{}"));
  139. if(dict) {
  140. if (dict[dKey] == domainOrUri) {
  141. GM_deleteValue(key);
  142. }
  143. }
  144. }
  145. document.location.reload(true);
  146. }
  147. }
  148. function resetUrlsForCurrentDomain() {
  149. resetUrlsForCurrentHelper("domain", document.domain)
  150. }
  151. function resetUrlsForCurrentSite() {
  152. resetUrlsForCurrentHelper("base", document.baseURI)
  153. }
  154.  
  155. function expireUrlsForCurrentSite() {
  156. // TODO: do this only once a day
  157. var keys = GM_listValues();
  158. if (!keys) {
  159. return;
  160. }
  161.  
  162. var cutOffDate = calcCutOffDate(defaultSettings.ageOfUrl);
  163.  
  164. for (var i=0, key=null; key=keys[i]; i++) {
  165. if (key == "lastExpire"){
  166. continue;
  167. }
  168. var dict = JSON.parse(GM_getValue(key, "{}"));
  169. if(dict) {
  170. try {
  171. // console.log(dict["domain"], cutOffDate.getTime(), dict["date"]);
  172. if (cutOffDate.getTime() > dict["date"]) {
  173. if (defaultSettings.expireAllDomains ||
  174. (dict["domain"] == document.domain))
  175. {
  176. GM_deleteValue(key);
  177. }
  178. }
  179. } catch (e) {
  180. console.log(e);
  181. }
  182. }
  183. else {
  184. console.log('Error! JSON.parse failed - dict is likely to be corrupted.');
  185. }
  186. }
  187. GM_setValue("lastExpire", new Date());
  188. }
  189.  
  190. function calcCutOffDate(age) {
  191. var cutOffDate = new Date();
  192. if (age >= 0) {
  193. cutOffDate.setHours(0,0,0,0);
  194. cutOffDate.setDate((new Date()).getDate() - age);
  195. }
  196. return cutOffDate;
  197. }
  198.  
  199. /*
  200. * Find the settings for a given URL
  201. */
  202. function findPerUrlSettings(theSettings, aDomain) {
  203. for (var i=0; i < theSettings.length; ++i) {
  204. var myRegExp = new RegExp(theSettings[i].url, 'i');
  205. if (aDomain.match(myRegExp)) {
  206. return theSettings[i];
  207. }
  208. }
  209. }
  210.  
  211. /*
  212. * Find the parent element as specified in the settings.
  213. */
  214. function locateParentElem(curSettings, aDomain, aRoot) {
  215. if (!curSettings) {
  216. return null;
  217. }
  218. // console.log("locateParentElem 1", curSettings.url);
  219. var res = null;
  220. for (xpath = 0; xpath < curSettings.parentHints.length; ++xpath) {
  221. // console.log("locateParentElem 2", curSettings.parentHints[xpath], aRoot);
  222. res = document.evaluate(curSettings.parentHints[xpath], aRoot, null, 9, null).singleNodeValue
  223. if (res) {
  224. // console.log("locateParentElem found something");
  225. return res;
  226. }
  227. }
  228. return res;
  229. }
  230. /*
  231. * Check if the current node qualifies for looking up the hierarchy.
  232. */
  233. function goUp(curSettings, aRoot) {
  234. if (!curSettings) {
  235. return null;
  236. }
  237.  
  238. res = null
  239. if (curSettings.upTrigger != "") {
  240. res = document.evaluate(curSettings.upTrigger, aRoot, null, 9, null).singleNodeValue
  241. }
  242. return res != null
  243. }
  244. function dimLinks() {
  245. interval = (1 - defaultSettings.targetOpacity4Dim)/defaultSettings.steps;
  246. countDownTimer = countDownTimer - 1;
  247. curOpacity = defaultSettings.targetOpacity4Dim + interval*countDownTimer;
  248.  
  249. for(var i = 0; i < theHRefs.length; i++)
  250. {
  251. var hash = 'm' + hex_md5(theHRefs[i].href);
  252. if (hash in dimMap) {
  253. theHRefs[i].style.opacity = curOpacity;
  254. }
  255. }
  256. if (countDownTimer > 0) {
  257. to = setTimeout(dimLinks, defaultSettings.dimInterval);
  258. }
  259. }
  260.  
  261. // Main part
  262.  
  263. // Menus
  264. GM_registerMenuCommand("Remove the seen history for this site.", resetUrlsForCurrentSite);
  265. GM_registerMenuCommand("Remove the seen history for this domain.", resetUrlsForCurrentDomain);
  266. GM_registerMenuCommand("Remove all seen history (for all sites)!", resetAllUrls);
  267.  
  268. function run_script() {
  269. // Vars
  270. var allHrefs = document.getElementsByTagName("a");
  271. var theBase = document.baseURI;
  272. var theDomain = document.domain;
  273. var elemMap = {};
  274. dimMap = {};
  275.  
  276. curSettings = findPerUrlSettings(perUrlSettings, theDomain);
  277. // console.log(curSettings);
  278.  
  279. // expire old data
  280. expireUrlsForCurrentSite();
  281.  
  282. // Change the DOM
  283.  
  284. // First loop: gather all new links and make already seen opaque.
  285. for(var i = 0; i < allHrefs.length; i++)
  286. {
  287. var hash = 'm' + hex_md5(allHrefs[i].href);
  288. // setValue needs letter in the beginning, thus use of 'm'
  289.  
  290. var key = GM_getValue(hash);
  291. // console.log(allHrefs[i].href, hash.toString());
  292.  
  293. if (typeof key != 'undefined') {
  294. // key found -> loaded this reference already
  295. done = false
  296. if(goUp(curSettings, allHrefs[i])) {
  297. pe = locateParentElem(curSettings, theDomain, allHrefs[i])
  298. // console.log("locate parent done", pe);
  299. if (pe) {
  300. pe.style.opacity = defaultSettings.targetOpacity;
  301. done = true
  302. }
  303. }
  304. if (!done) {
  305. // change display
  306. allHrefs[i].style.opacity = defaultSettings.targetOpacity;
  307. }
  308. } else {
  309. // key not found, store it with current date
  310. elemMap[hash] = {"domain":theDomain, "date":(new Date()).getTime(), "base":theBase};
  311. dimMap[hash] = allHrefs[i];
  312. }
  313. }
  314.  
  315. // remember all new urls to hide the next time
  316. for (var e2 in elemMap) {
  317. GM_setValue(e2, JSON.stringify(elemMap[e2]));
  318. }
  319. theHRefs = allHrefs;
  320. to = setTimeout(dimLinks, defaultSettings.dimInterval);
  321. }
  322.  
  323. run_script();
  324. //})();