Hackernews Modern

Improved mobile usability and modern styling for Hackernews

目前为 2022-08-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Hackernews Modern
  3. // @namespace sagiegurari
  4. // @version 1.5
  5. // @author Sagie Gur-Ari
  6. // @description Improved mobile usability and modern styling for Hackernews
  7. // @homepage https://github.com/sagiegurari/userscripts-hackernews
  8. // @supportURL https://github.com/sagiegurari/userscripts-hackernews/issues
  9. // @match https://news.ycombinator.com/*
  10. // @grant none
  11. // @license MIT License
  12. // ==/UserScript==
  13.  
  14. (function run() {
  15. 'use strict';
  16.  
  17. const element = document.createElement('style');
  18. element.type = 'text/css';
  19. document.head.appendChild(element);
  20. const styleSheet = element.sheet;
  21.  
  22. const cssRules = [
  23. // defaults
  24. '.subtext .age a[href^="item"] { color: #828282; }',
  25.  
  26. // colors
  27. '#hnmain tr:first-child td, .comment-tree { background-color: #333; }',
  28. 'html, body, #hnmain, #hnmain table.itemList tr:first-child td { background-color: #222; }',
  29. 'a:link, .subtext a[href^="item"]:not(:visited), a:link.togg.clicky, .commtext, .comment-tree a[rel="nofollow"], .comment-tree .reply a { color: #eee; }',
  30. '.visited a.titlelink { color: #888; }',
  31. ];
  32.  
  33. // if mobile or emulator
  34. if (navigator.userAgent.toLowerCase('android') !== -1 || navigator.userAgentData.mobile) {
  35. cssRules.push(...[
  36. // styles
  37. '.pagetop { font-size: 16pt; }',
  38. '.title { font-size: 14pt; }',
  39. '.comhead { font-size: 12pt; }',
  40. '.subtext { font-size: 0; padding: 5px 0; }',
  41. '.subtext span { padding: 0 2px; }',
  42. '.subtext span, .subtext a:not([href^="item"]), .subtext .age a[href^="item"] { font-size: 12pt; text-decoration: none; }',
  43. '.subtext a[href^="item"] { font-size: 14pt; text-decoration: underline; }',
  44. '.subtext a[href^="hide"] { display: none; }',
  45. '.default { font-size: 12pt }',
  46. ]);
  47. }
  48.  
  49. cssRules.forEach(cssRule => {
  50. styleSheet.insertRule(cssRule, styleSheet.cssRules.length);
  51. });
  52.  
  53. // collapse non top comments
  54. document.querySelectorAll('.ind:not([indent="0"])').forEach(topCommentIndent => {
  55. topCommentIndent.parentElement.querySelectorAll('.togg.clicky').forEach(toggle => toggle.click());
  56. });
  57.  
  58. const storage = window.localStorage;
  59. if (storage && typeof storage.getItem === 'function') {
  60. const KEY = 'hn-cache-visited';
  61. const CACHE_LIMIT = 100;
  62.  
  63. const readFromCache = () => {
  64. const listStr = storage.getItem(KEY);
  65.  
  66. if (!listStr) {
  67. return [];
  68. }
  69.  
  70. return listStr.split(',');
  71. };
  72. const writeToCache = (ids) => {
  73. if (!ids || !Array.isArray(ids) || !ids.length) {
  74. return;
  75. }
  76.  
  77. // add to start
  78. cache.unshift(...ids);
  79.  
  80. // remove duplicates
  81. const seen = {};
  82. cache = cache.filter(function (item) {
  83. if (seen[item]) {
  84. return false;
  85. }
  86.  
  87. seen[item] = true;
  88. return true;
  89. });
  90.  
  91. // trim
  92. const extraCount = cache.length - CACHE_LIMIT;
  93. if (extraCount) {
  94. cache.splice(cache.length - extraCount, extraCount);
  95. }
  96.  
  97. storage.setItem(KEY, cache.join(','));
  98. };
  99.  
  100. let cache = readFromCache();
  101.  
  102. // mark visited
  103. const markVisited = () => {
  104. const elements = document.querySelectorAll('tr.athing');
  105.  
  106. for (let index = 0; index < elements.length; index++) {
  107. const element = elements[index];
  108. if (cache.indexOf(element.id) !== -1) {
  109. element.classList.add('visited');
  110. }
  111. }
  112. };
  113. markVisited();
  114.  
  115. // listen to scroll and add to cache
  116. const markVisibleAsVisited = () => {
  117. const elements = document.querySelectorAll('tr.athing:not(.visited)');
  118.  
  119. let started = false;
  120. const ids = [];
  121. for (let index = 0; index < elements.length; index++) {
  122. const element = elements[index];
  123. const bounding = element.getBoundingClientRect();
  124. if (bounding.top >= 0 &&
  125. bounding.bottom <= window.innerHeight) {
  126. started = true;
  127. ids.push(element.id);
  128. } else if (started) {
  129. break;
  130. }
  131. }
  132.  
  133. if (ids.length) {
  134. writeToCache(ids);
  135. }
  136. };
  137. let timeoutID = null;
  138. document.addEventListener('scroll', () => {
  139. clearTimeout(timeoutID);
  140. timeoutID = setTimeout(markVisibleAsVisited, 25);
  141. }, {
  142. passive: true
  143. });
  144.  
  145. markVisibleAsVisited();
  146. }
  147. }());