├── README.md ├── hackem.js ├── hackemtimer.js ├── hackemup.css ├── hackemup.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # Hack'em Up: A Hacker News bookmarklet 2 | 3 | Welcome to a Hacker News Bookmarklet... 4 | "Hack'em Up" by Mr Speaker 5 | v1.1 6 | 7 | Screen play: 8 | Drag bookmarklet to a tab that is opened to 9 | http://www.ycombinator.com/news 10 | Every 2 minutes the page will be refreshed and 11 | changes (to ranks, comments, votes, karma) 12 | will be highlighted. 13 | 14 | Written and directed by: 15 | var _ = mrspeaker 16 | twitter = @_ 17 | mail = _@gmail.com, 18 | tubes = http://_.net; 19 | 20 | Also staring: 21 | jQuery Bookmarklet - version 1.0 22 | Originally written by: Brett Barros 23 | With modifications by: Paul Irish 24 | 25 | Change Log: 26 | 1.1 27 | Refactored away some local state. 28 | General clean ups. 29 | 30 | -------------------------------------------------------------------------------- /hackem.js: -------------------------------------------------------------------------------- 1 | /* 2 | Welcome to a Hacker News Bookmarklet... 3 | "Hack'em Up" by Mr Speaker 4 | v1.1 5 | 6 | jQuery Bookmarklet loader/initialiser 7 | */ 8 | 9 | // Loadem Up! 10 | (function(opts){fullFunc(opts)})({ 11 | css : [hnuBase + "hackemup.css?v=2"], 12 | js : [ 13 | hnuBase + "hackemup.js", 14 | hnuBase + "hackemtimer.js" 15 | ], 16 | ready : function() { 17 | 18 | // Only works on the main page 19 | var loc = window.document.location; 20 | if(loc.hostname !== "news.ycombinator.com" || (loc.pathname !== "/" && loc.pathname !== "/news") ){ 21 | alert("Only works on Hacker News front page:\nhttp://news.ycombinator.com/"); 22 | return; 23 | }; 24 | 25 | // Start the show. 26 | hackemup.init(); 27 | hnutimer.init(function() { 28 | // When the timer's done... 29 | hackemup.fetch(); 30 | }); 31 | 32 | // Open all links in a new tab. 33 | // ... I don't usually like to do such a thing, but by public demand... 34 | $("body a").live("click", function(){ 35 | $(this).attr("target", "_blank"); 36 | }); 37 | } 38 | }); 39 | 40 | // jQuery bookmarklet magic... 41 | // ... by Brett Barros (& Paul Irish) 42 | // ... http://www.latentmotion.com/downloads/blank-bookmarklet-v1.js 43 | function fullFunc(a){function d(b){if(b.length===0){a.ready();return false} 44 | $.getScript(b[0],function(){d(b.slice(1))})}function e(b){$.each(b,function(c,f){$("") 45 | .attr({href:f,rel:"stylesheet",type:'text/css'}).appendTo("head")})}a.jqpath=a. 46 | jqpath||"https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"; 47 | (function(b){var c=document.createElement("script");c.type="text/javascript";c.src=b; 48 | c.onload=function(){e(a.css);d(a.js)};document.body.appendChild(c)})(a.jqpath)}; 49 | -------------------------------------------------------------------------------- /hackemtimer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Welcome to a Hacker News Bookmarklet... 3 | "Hack'em Up" by Mr Speaker 4 | v1.1 5 | 6 | Timer module: Will only update when tab is focused. 7 | */ 8 | var hnutimer = { 9 | refreshTime: 2 * (60 * 1000), 10 | waitOnFocusTime: 1500, 11 | 12 | timerId: null, 13 | lastCheck: null, 14 | focusedTime: null, 15 | 16 | init: function(onTimerExpire) { 17 | $(window).bind({ 18 | "focus": function(){ hnutimer.onFocus(); }, 19 | "blur": function(){ hnutimer.onBlur(); } 20 | }); 21 | this.onTimerExpire = onTimerExpire; 22 | // Init onfocus to avoid first time load delay 23 | this.focusedTime = new Date().getTime() - this.waitOnFocusTime; 24 | this.update(); 25 | }, 26 | update: function() { 27 | var doFetch = true, 28 | refreshTime = this.refreshTime, 29 | previous = this.lastCheck, 30 | rightNow = new Date().getTime(), 31 | elapsed = previous ? rightNow - previous : refreshTime; 32 | 33 | if (elapsed < refreshTime) { 34 | doFetch = false; 35 | refreshTime -= elapsed; 36 | } 37 | 38 | if(doFetch) { 39 | if(rightNow - this.focusedTime >= this.waitOnFocusTime){ 40 | this.onTimerExpire && this.onTimerExpire(); 41 | this.lastCheck = rightNow; 42 | } else { 43 | refreshTime = this.waitOnFocusTime; 44 | } 45 | } 46 | this.timerId = setTimeout(function(){ hnutimer.update(); }, refreshTime); 47 | }, 48 | onFocus: function() { 49 | this.focusedTime = new Date().getTime(); 50 | this.update(); 51 | }, 52 | onBlur: function() { 53 | clearTimeout(this.timerId); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /hackemup.css: -------------------------------------------------------------------------------- 1 | del.hnu { 2 | text-decoration: none; 3 | } 4 | .hnu { 5 | font-size:0.9em; 6 | background-color: #f60; 7 | } 8 | .hnu-col { 9 | min-width: 35px; 10 | } 11 | .hnu-up, .hnu-down, .hnu-new { 12 | -webkit-border-radius: 6px; 13 | -moz-border-radius: 6px; 14 | border-radius: 6px; 15 | padding: 1px 3px; 16 | color: #fff; 17 | float:left; 18 | margin: 3px 1px 0 2px; 19 | font-size: 0.6em; 20 | min-width: 6px; 21 | text-align: center; 22 | } 23 | .hnu-up { background-color: #86c444; } 24 | .hnu-down { background-color:#e76363; opacity: 0.6; } 25 | .hnu-votes { 26 | padding: 0 2px; 27 | color: #f60; 28 | background-color: transparent; 29 | } 30 | .hnu-karma { 31 | color: #f6f6f6; 32 | padding: 0 2px; 33 | font-size: 0.7em; 34 | } 35 | 36 | .hnu-spin { 37 | -o-transform: rotate(1440deg); 38 | -o-transition: all 6s linear; 39 | 40 | -moz-transform: rotate(1440deg); 41 | -moz-transition: all 6s linear; 42 | 43 | -webkit-transform: rotate(1440deg); 44 | -webkit-transition: all 6s linear; 45 | 46 | transform: rotate(1440deg); 47 | transition: transform 6s linear; 48 | } -------------------------------------------------------------------------------- /hackemup.js: -------------------------------------------------------------------------------- 1 | /* 2 | Welcome to a Hacker News Bookmarklet... 3 | "Hack'em Up" by Mr Speaker 4 | v1.1 5 | 6 | DOM wranglin' a go-go. 7 | */ 8 | var hackemup = { 9 | 10 | selecta: { 11 | body: "body table:first", 12 | logo: "body table:first table:first tbody tr:first td:first a:first img", 13 | firstColumn: "body > center > table tbody tr:eq(3) td > table > tbody > tr > td:first" 14 | }, 15 | 16 | init: function() { 17 | // Animate the first column 18 | $(this.selecta.firstColumn) 19 | .animate({ width: 35 }, 400); 20 | }, 21 | 22 | fetch: function(isRetry) { 23 | var _this = this, 24 | logo = $(this.selecta.logo).addClass("hnu-spin"); 25 | 26 | // Fetch the new HTML 27 | $("
").load("/ " + this.selecta.body, function(response, status) { 28 | logo.removeClass("hnu-spin"); 29 | 30 | if(status == "success") { 31 | _this.update($(response)); 32 | return; 33 | } 34 | 35 | // Retry once 36 | !isRetry && setTimeout(function(){ 37 | _this.fetch(true); 38 | }, 1500); 39 | return; 40 | }); 41 | }, 42 | 43 | update: function(fetched) { 44 | // Remove added changes from last round 45 | // (because we re-parse the doc. TODO: just store the last doc 46 | // then we don't have to be careful about how we add new elements) 47 | $(this.selecta.body).find(".hnu").remove(); 48 | 49 | // Extract some infoz 50 | var lastDoc = new hndoc($(this.selecta.body)), 51 | newDoc = new hndoc(fetched.children()), 52 | _this = this; 53 | 54 | // Replace the current page DOM with the latest DOM 55 | lastDoc.$.replaceWith(newDoc.$); 56 | 57 | // Stretch the first column 58 | $(this.selecta.firstColumn).addClass("hnu-col"); 59 | 60 | // Check if articles have changed 61 | newDoc 62 | .articleList 63 | .each(function() { 64 | var newVersion = this, 65 | oldVersion = lastDoc 66 | .articleList 67 | .filter(function(){ 68 | return newVersion.id === this.id; 69 | }); 70 | 71 | if(oldVersion.length) { 72 | _this.updateArticle(newVersion, oldVersion[0]); 73 | } 74 | else { 75 | _this.newArticle(newVersion); 76 | } 77 | }); 78 | 79 | // Hide runs of rises (probably means another story tanked) 80 | this.removeRuns(newDoc.$.find(".hnu-up")); 81 | this.removeRuns(newDoc.$.find(".hnu-down")); 82 | 83 | // Check if karma has changed 84 | if(newDoc.karma !== lastDoc.karma) { 85 | this.setKarma(newDoc, lastDoc.karma); 86 | } 87 | }, 88 | 89 | // Update the DOM to show last karma 90 | setKarma: function(doc, oldKarma) { 91 | var end = $(document.createTextNode(") | ")), 92 | old = $("") 93 | .text(oldKarma) 94 | .addClass("hnu hnu-karma"); 95 | 96 | doc.$karma().textContent = " (" + doc.karma; 97 | old.insertAfter(doc.$karma()); 98 | end.insertAfter(old); 99 | }, 100 | 101 | // Update the DOM to include the last lot of info 102 | updateArticle: function(newDoc, oldDoc) { 103 | // Article rose (or sank a lot) 104 | if(newDoc.rank < oldDoc.rank || newDoc.rank - oldDoc.rank > 4) { 105 | $("") 106 | .addClass("hnu") 107 | .addClass(newDoc.rank > oldDoc.rank ? "hnu-down" : "hnu-up") 108 | .text(Math.abs(oldDoc.rank - newDoc.rank)) 109 | .hide() 110 | .prependTo(newDoc.$rank()) 111 | .fadeIn(); 112 | } 113 | 114 | // Votes changed 115 | if(newDoc.votes !== oldDoc.votes) { 116 | $("") 117 | .addClass("hnu hnu-votes") 118 | .text(oldDoc.votes) 119 | .insertBefore(newDoc.$votes()); 120 | } 121 | 122 | // Comment count change 123 | if(newDoc.comments !== oldDoc.comments) { 124 | $("") 125 | .addClass('hnu hnu-votes') 126 | .text(oldDoc.comments) 127 | .insertBefore(newDoc.$comments()); 128 | } 129 | }, 130 | 131 | // Update the DOM to show the article is new 132 | newArticle: function(newDoc) { 133 | $("") 134 | .addClass("hnu hnu-new") 135 | .text("+") 136 | .hide() 137 | .prependTo(newDoc.$rank()) 138 | .fadeIn(); 139 | }, 140 | 141 | // Remove runs of +1 rises (when another story nose-dives) 142 | removeRuns: function(elements) { 143 | var extractValuesFromElement = function(el) { 144 | return { 145 | rise: parseInt($(el).text(), 10), 146 | rank: parseInt(el.nextSibling.textContent.replace(/[^0-9]/g,""), 10), 147 | $: $(el) 148 | }; 149 | }, 150 | sortByRank = function(a, b) { return a.rank - b.rank; }, 151 | groupIntoRuns = function(acc, el) { 152 | var head = acc.length ? acc[acc.length - 1] : [], 153 | isSeq = function(prev, el){ 154 | return el.rise === prev.rise && 155 | el.rank === prev.rank + 1; 156 | }; 157 | if(head.length && isSeq(head[head.length - 1], el)) { 158 | head.push(el); 159 | } 160 | else { 161 | acc.push([el]); 162 | } 163 | return acc; 164 | }, 165 | returnAnyLongRuns = function(el) { return el.length > 2; }, 166 | flattenRuns = function(acc, el) { return acc.concat(el); }, 167 | removeIndicator = function(el) { 168 | el.$.fadeOut("fast", function() { 169 | $(this).remove(); 170 | }); 171 | }; 172 | 173 | // Get all map/reduce-y on the green circles 174 | elements 175 | .get() 176 | .map( extractValuesFromElement ) 177 | .sort( sortByRank ) 178 | .reduce( groupIntoRuns, [] ) 179 | .filter( returnAnyLongRuns ) 180 | .reduce( flattenRuns, [] ) 181 | .forEach( removeIndicator ); 182 | } 183 | }; 184 | // Encapsulate an entire HN page 185 | function hndoc($doc) { 186 | this.$ = $doc; 187 | 188 | var cache = {}; 189 | this.$header = function() { 190 | return cache.header || 191 | (cache.header = this.$.find("tbody tr:eq(0) table")); 192 | }; 193 | this.$articles = function() { 194 | return cache.articles || 195 | (cache.articles = this.$.find("tbody tr:eq(3) table tr").slice(0, -2)); 196 | }; 197 | this.$userNode = function(){ 198 | return cache.userNode || 199 | (cache.userNode = this.$header().find("tr > td:last").children()); 200 | }; 201 | this.$karma = function(){ 202 | return cache.karma || 203 | (cache.karma = this.$userNode().contents()[1]); 204 | }; 205 | 206 | this.isLoggedIn = this.$userNode().find("a:first").text().indexOf("login") === -1; 207 | this.karma = ! this.isLoggedIn ? -1 : parseInt(this.$karma().textContent.replace(/[^0-9]/g,""), 10); 208 | this.articleList = this.$articles() 209 | .filter(function(ind) { 210 | // Every third TR is the start of an article 211 | return ind % 3 === 0; 212 | }) 213 | .map(function() { 214 | // Turn them into articles 215 | return new hnarticle($(this)); 216 | }); 217 | } 218 | 219 | // Encapsulate an individual article 220 | function hnarticle($row) { 221 | this.$ = $row; 222 | 223 | var cache = {}; 224 | this.$rank = function(){ 225 | return cache.rank || 226 | (cache.rank = this.$.find("td:first")); 227 | }; 228 | this.$votes = function() { 229 | return cache.votes || 230 | (cache.votes = this.$.next().find("td:eq(1) span:first")); 231 | }; 232 | this.$comments = function() { 233 | return cache.comments || 234 | (cache.comments = this.$.next().find("td:eq(1) a:last")); 235 | }; 236 | this.$posted = function() { 237 | var contents = this.$.next().find("td:eq(1)").contents(); 238 | // If ! length > 3 then it's a "special" YC post 239 | return cache.posted || 240 | (cache.posted = contents.length > 3 ? contents[3] : contents[0]); 241 | }; 242 | 243 | this.id = parseInt((this.$.next().find("td:eq(1) span").attr("id") + "").slice(6), 10); 244 | this.rank = parseInt(this.$rank().text().replace(/[^0-9]/g,""), 10); 245 | this.votes = parseInt(this.$votes().text(), 10); 246 | this.comments = (parseInt(this.$comments().text(), 10) || 0); 247 | this.posted = this.$posted().textContent; 248 | // Not grabbed: postedBy, article Name, URL 249 | } 250 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HackemUp: By Mr Speaker. A HackerNews bookmarklet 5 | 88 | 89 | 90 |
91 |

HackemUp: A Hacker News bookmarklet

92 |

93 | Keep track of what's changed on HackerNews main page since the last time you looked. 94 |

95 |

96 | 1 Show HN: This is what a rising article looks like
97 | 4 Netcraft confirms that this article is dying fast.
98 | + New article released. Underneath you can see the updated stats.
99 |

100 | 75 101 | 88 points by mrspeaker 102 | 3 hours ago | 103 | 8 104 | 14 comments 105 |
106 | 107 |

108 |

109 | 114 | HackemUp - A HN Bookmarklet 115 | 116 |

117 |

Highlights and updates new articles, articles that are rising or falling fast (drop more than 3), as well as updating comment counts, 118 | points, and your karma. Updates every couple of minutes - but if you move to another tab (and leave HN open) no updates will happen until you get back.

119 |

Directions 120 |

    121 |
  1. Open Hacker News.
  2. 122 |
  3. Run the bookmarklet above on the HN page (in Chrome, just drag the link onto the tab - for others, drag the link to the bookmark bar, then switch to NH, then click the bookmark link.)
  4. 123 |
  5. Read HN.
  6. 124 |
  7. Every couple of minutes, the page will magically refresh, with changes displayed.
  8. 125 |
126 |

127 |

128 | More info at Mr Speaker's Hompage. 129 |

130 |

 

131 |
132 | 133 | --------------------------------------------------------------------------------