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

  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.41
  7. // @lastchanges multiple urls for settings
  8. // @license GNU GPL version 3
  9. // @released 2014-02-20
  10. // @updated 2014-08-31
  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. cleanOnlyDaily: true
  86. }
  87.  
  88. var perUrlSettings = [
  89. {
  90. url : ['.*\.?derstandard\.at', '.*\.?diestandard\.at', '.*\.?dastandard\.at' ],
  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[@rel='bookmark']",
  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. var keyLastExpireOp = "lastExpire";
  123.  
  124. function resetAllUrls() {
  125. if (confirm('Are you sure you want to erase the complete seen history?')) {
  126. var keys = GM_listValues();
  127. for (var i=0, key=null; key=keys[i]; i++) {
  128. GM_deleteValue(key);
  129. }
  130. document.location.reload(true);
  131. }
  132. }
  133.  
  134. function resetUrlsForCurrentHelper(dKey, domainOrUri) {
  135. if (confirm('Are you sure you want to erase the seen history for ' +
  136. domainOrUri + '?')) {
  137. var keys = GM_listValues();
  138. for (var i=0, key=null; key=keys[i]; i++) {
  139. var dict = JSON.parse(GM_getValue(key, "{}"));
  140. if(dict) {
  141. if (dict[dKey] == domainOrUri) {
  142. GM_deleteValue(key);
  143. }
  144. }
  145. }
  146. document.location.reload(true);
  147. }
  148. }
  149. function resetUrlsForCurrentDomain() {
  150. resetUrlsForCurrentHelper("domain", document.domain)
  151. }
  152. function resetUrlsForCurrentSite() {
  153. resetUrlsForCurrentHelper("base", document.baseURI)
  154. }
  155.  
  156. function expireUrls() {
  157. if (defaultSettings.cleanOnlyDaily) {
  158. lastExpireDate = new Date(GM_getValue(keyLastExpireOp, nDaysOlderFromNow(2)));
  159. diff = Math.abs((new Date()) - lastExpireDate);
  160. if (diff / 1000 / 3600 / 24 < 1) {
  161. // less than one day -> no DB cleaning
  162. return;
  163. }
  164. }
  165.  
  166. var keys = GM_listValues();
  167. if (!keys) {
  168. return;
  169. }
  170.  
  171. var cutOffDate = nDaysOlderFromNow(defaultSettings.ageOfUrl);
  172.  
  173. for (var i=0, key=null; key=keys[i]; i++) {
  174. if (key == keyLastExpireOp){
  175. continue;
  176. }
  177. var dict = JSON.parse(GM_getValue(key, "{}"));
  178. if(dict) {
  179. try {
  180. // console.log(dict["domain"], cutOffDate.getTime(), dict["date"]);
  181. if (cutOffDate.getTime() > dict["date"]) {
  182. if (defaultSettings.expireAllDomains ||
  183. (dict["domain"] == document.domain))
  184. {
  185. GM_deleteValue(key);
  186. }
  187. }
  188. } catch (e) {
  189. console.log(e);
  190. }
  191. }
  192. else {
  193. console.log('Error! JSON.parse failed - dict is likely to be corrupted.');
  194. }
  195. }
  196. GM_setValue(keyLastExpireOp, new Date());
  197. }
  198.  
  199. function nDaysOlderFromNow(age, aDate, zeroHour) {
  200. aDate = typeof aDate !== 'undefined' ? aDate : new Date();
  201. zeroHour = typeof zeroHour !== 'undefined' ? zeroHour : true;
  202. var dateStore = new Date(aDate.getTime());
  203. var workDate = aDate;
  204. if (age >= 0) {
  205. workDate.setDate(dateStore.getDate() - age);
  206. if (zeroHour) {
  207. workDate.setHours(0,0,0,0);
  208. }
  209. }
  210. return workDate;
  211. }
  212.  
  213. /*
  214. * Find the settings for a given URL
  215. */
  216. function findPerUrlSettings(theSettings, aDomain) {
  217. for (var i=0; i < theSettings.length; ++i) {
  218. for (var j = 0; j < theSettings[i].url.length; ++j) {
  219. var myRegExp = new RegExp(theSettings[i].url[j], 'i');
  220. if (aDomain.match(myRegExp)) {
  221. return theSettings[i];
  222. }
  223. }
  224. }
  225. }
  226.  
  227. /*
  228. * Find the parent element as specified in the settings.
  229. */
  230. function locateParentElem(curSettings, aDomain, aRoot) {
  231. if (!curSettings) {
  232. return null;
  233. }
  234. // console.log("locateParentElem 1", curSettings.url);
  235. var res = null;
  236. for (xpath = 0; xpath < curSettings.parentHints.length; ++xpath) {
  237. // console.log("locateParentElem 2", curSettings.parentHints[xpath], aRoot);
  238. res = document.evaluate(curSettings.parentHints[xpath], aRoot, null, 9, null).singleNodeValue;
  239. if (res) {
  240. // console.log("locateParentElem found something");
  241. return res;
  242. }
  243. }
  244. return res;
  245. }
  246. /*
  247. * Check if the current node qualifies for looking up the hierarchy.
  248. */
  249. function goUp(curSettings, aRoot) {
  250. if (!curSettings) {
  251. return null;
  252. }
  253.  
  254. res = null;
  255. if (curSettings.upTrigger != "") {
  256. res = document.evaluate(curSettings.upTrigger, aRoot, null, 9, null).singleNodeValue;
  257. }
  258. return res != null
  259. }
  260. function dimLinks() {
  261. interval = (1 - defaultSettings.targetOpacity4Dim)/defaultSettings.steps;
  262. countDownTimer = countDownTimer - 1;
  263. curOpacity = defaultSettings.targetOpacity4Dim + interval*countDownTimer;
  264.  
  265. for(var i = 0; i < theHRefs.length; i++)
  266. {
  267. var hash = 'm' + hex_md5(theHRefs[i].href);
  268. if (hash in dimMap) {
  269. theHRefs[i].style.opacity = curOpacity;
  270. }
  271. }
  272. if (countDownTimer > 0) {
  273. to = setTimeout(dimLinks, defaultSettings.dimInterval);
  274. }
  275. }
  276.  
  277. // Main part
  278.  
  279. // Menus
  280. GM_registerMenuCommand("Remove the seen history for this site.", resetUrlsForCurrentSite);
  281. GM_registerMenuCommand("Remove the seen history for this domain.", resetUrlsForCurrentDomain);
  282. GM_registerMenuCommand("Remove all seen history (for all sites)!", resetAllUrls);
  283.  
  284. function run_script() {
  285. // Vars
  286. var allHrefs = document.getElementsByTagName("a");
  287. var theBase = document.baseURI;
  288. var theDomain = document.domain;
  289. var elemMap = {};
  290. dimMap = {};
  291.  
  292. curSettings = findPerUrlSettings(perUrlSettings, theDomain);
  293. // console.log(curSettings);
  294.  
  295. // expire old data
  296. expireUrls();
  297.  
  298. // Change the DOM
  299.  
  300. // First loop: gather all new links and make already seen opaque.
  301. for(var i = 0; i < allHrefs.length; i++)
  302. {
  303. var hash = 'm' + hex_md5(allHrefs[i].href);
  304. // setValue needs letter in the beginning, thus use of 'm'
  305.  
  306. var key = GM_getValue(hash);
  307. // console.log(allHrefs[i].href, hash.toString());
  308.  
  309. if (typeof key != 'undefined') {
  310. // key found -> loaded this reference already
  311. done = false;
  312. if(goUp(curSettings, allHrefs[i])) {
  313. pe = locateParentElem(curSettings, theDomain, allHrefs[i])
  314. // console.log("locate parent done", pe);
  315. if (pe) {
  316. pe.style.opacity = defaultSettings.targetOpacity;
  317. done = true;
  318. }
  319. }
  320. if (!done) {
  321. // change display
  322. allHrefs[i].style.opacity = defaultSettings.targetOpacity;
  323. }
  324. } else {
  325. // key not found, store it with current date
  326. elemMap[hash] = {"domain":theDomain, "date":(new Date()).getTime(), "base":theBase};
  327. dimMap[hash] = allHrefs[i];
  328. }
  329. }
  330.  
  331. // remember all new urls to hide the next time
  332. for (var e2 in elemMap) {
  333. GM_setValue(e2, JSON.stringify(elemMap[e2]));
  334. }
  335. theHRefs = allHrefs;
  336. to = setTimeout(dimLinks, defaultSettings.dimInterval);
  337. }
  338.  
  339. run_script();
  340. //})();