├── .github └── FUNDING.yml ├── .gitignore ├── 404.html ├── CNAME ├── Gruntfile.js ├── README.md ├── assets ├── css │ ├── hw-ios-2.css │ ├── hw-ios-2.min.css │ ├── hw-ios.css │ ├── hw-ios.min.css │ ├── hw-web.css │ └── hw-web.min.css ├── images │ ├── comment-arrow.png │ ├── external-arrow.png │ ├── ios │ │ ├── activity-indicator.png │ │ ├── back-button-5.png │ │ ├── back-button-active-5.png │ │ ├── back-button-active.png │ │ ├── back-button.png │ │ ├── button-5.png │ │ ├── button-active-5.png │ │ ├── button-active.png │ │ ├── button-bar-press-indicator.png │ │ ├── button.png │ │ ├── close-button.png │ │ ├── detail-disclosure-button-active.png │ │ ├── detail-disclosure-button.png │ │ ├── disclosure-indicator-active.png │ │ ├── disclosure-indicator.png │ │ ├── nav-bar-5.png │ │ ├── nav-bar-mini-5.png │ │ ├── nav-bar-mini.png │ │ ├── nav-bar.png │ │ ├── refresh-icon.png │ │ ├── silver-button-active.png │ │ ├── silver-button-bar.png │ │ ├── silver-button.png │ │ └── silver-nav-bar.png │ ├── ios7 │ │ ├── back-icon.png │ │ ├── close-button.png │ │ ├── comment-arrow.png │ │ ├── comment-button.png │ │ ├── disclosure-indicator.png │ │ ├── external-arrow.png │ │ └── refresh-icon.png │ └── web │ │ ├── comment-arrow.svg │ │ ├── comments-icon.png │ │ ├── comments-icon.svg │ │ ├── external-arrow.svg │ │ ├── home-icon.png │ │ ├── home-icon.svg │ │ ├── refresh-icon.png │ │ └── refresh-icon.svg ├── js │ ├── hw-ios-2.js │ ├── hw-ios.js │ ├── hw-web.js │ ├── hw.js │ ├── libs │ │ ├── amplify.store.js │ │ ├── classList.js │ │ ├── hnapi.js │ │ ├── hogan.js │ │ ├── ibento.js │ │ ├── requestanimationframe.js │ │ ├── ruto.js │ │ ├── tappable.js │ │ └── tween.js │ └── templates.js └── templates │ ├── comments-toggle.mustache │ ├── comments.mustache │ ├── post-comments.mustache │ ├── post.mustache │ └── stories-load.mustache ├── chrome-web-app ├── icon-128.png └── manifest.json ├── icons ├── favicon-128.png ├── favicon-196.png ├── favicon-32.png ├── favicon.ico ├── favicon.png ├── fxos-icon-128.png ├── fxos-icon-30.png ├── fxos-icon-60.png ├── icon-128.png ├── icon-180.png ├── icon-196.png ├── icon-30.png ├── icon-60.png ├── icon.svg ├── touch-icon-114.png ├── touch-icon-120.png ├── touch-icon-144.png ├── touch-icon-152.png ├── touch-icon-72.png └── touch-icon-76.png ├── index.html ├── js ├── hnapi-worker.js ├── hw-ios-2.min.js ├── hw-ios-2.min.js.map ├── hw-ios.min.js ├── hw-ios.min.js.map ├── hw-web.min.js └── hw-web.min.js.map ├── manifest.json ├── manifest.webapp ├── options.html ├── package-lock.json ├── package.json ├── promo-images └── promo-440-280.png ├── screenshots ├── screenshot-chrome.png ├── screenshot-fxos-1.png ├── screenshot-fxos-2.png ├── screenshot1.png ├── screenshot1@2x.png ├── screenshot2.png └── screenshot2@2x.png ├── script └── server ├── service-worker.js ├── sources ├── hackerweb-logo.xar ├── ios-cubic-bezier.xar ├── ios-elements.xar ├── ios7-elements.xar ├── landing.xar ├── loader.xar ├── misc.xar ├── promo-image.afdesign └── ycombinator-logo.xar ├── startups ├── startup-320-460.png └── startup-640-920.png └── tasks ├── embedImage.js └── templates.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cheeaun # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.buymeacoffee.com/cheeaun # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 – Page not found. 5 | 8 |

Page not found. Go to HackerWeb.

9 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | hackerweb.app -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | uglify: { 6 | web: { 7 | options: { 8 | sourceMap: 'js/hw-web.min.js.map', 9 | output: { 10 | max_line_len: 500, 11 | }, 12 | }, 13 | files: { 14 | 'js/hw-web.min.js': [ 15 | 'assets/js/libs/ruto.js', 16 | 'assets/js/libs/amplify.store.js', 17 | 'assets/js/libs/hogan.js', 18 | 'assets/js/libs/hnapi.js', 19 | 'assets/js/libs/ibento.js', 20 | 'assets/js/templates.js', 21 | 'assets/js/hw.js', 22 | 'assets/js/hw-web.js' 23 | ] 24 | } 25 | }, 26 | ios: { 27 | options: { 28 | sourceMap: 'js/hw-ios.min.js.map', 29 | output: { 30 | max_line_len: 500, 31 | }, 32 | }, 33 | files: { 34 | 'js/hw-ios.min.js': [ 35 | 'assets/js/libs/ruto.js', 36 | 'assets/js/libs/amplify.store.js', 37 | 'assets/js/libs/hogan.js', 38 | 'assets/js/libs/hnapi.js', 39 | 'assets/js/libs/tappable.js', 40 | 'assets/js/libs/tween.js', 41 | 'assets/js/libs/requestanimationframe.js', 42 | 'assets/js/templates.js', 43 | 'assets/js/hw.js', 44 | 'assets/js/hw-ios.js' 45 | ] 46 | } 47 | }, 48 | ios2: { 49 | options: { 50 | sourceMap: 'js/hw-ios-2.min.js.map', 51 | output: { 52 | max_line_len: 500, 53 | }, 54 | }, 55 | files: { 56 | 'js/hw-ios-2.min.js': [ 57 | 'assets/js/libs/ruto.js', 58 | 'assets/js/libs/amplify.store.js', 59 | 'assets/js/libs/hogan.js', 60 | 'assets/js/libs/hnapi.js', 61 | 'assets/js/libs/tappable.js', 62 | 'assets/js/libs/tween.js', 63 | 'assets/js/libs/requestanimationframe.js', 64 | 'assets/js/templates.js', 65 | 'assets/js/hw.js', 66 | 'assets/js/hw-ios-2.js' 67 | ] 68 | } 69 | } 70 | }, 71 | cssmin: { 72 | target: { 73 | files: [{ 74 | expand: true, 75 | cwd: 'assets/css', 76 | src: ['*.css', '!*.min.css'], 77 | dest: 'assets/css', 78 | ext: '.min.css' 79 | }] 80 | } 81 | }, 82 | templates: { 83 | all: { 84 | files: { 85 | 'assets/js/templates.js': [ 86 | 'assets/templates/*.mustache' 87 | ] 88 | } 89 | } 90 | }, 91 | watch: { 92 | scripts: { 93 | files: [ 94 | 'assets/js/libs/*.js', 95 | 'assets/js/*.js', 96 | 'Gruntfile.js' 97 | ], 98 | tasks: ['uglify'] 99 | }, 100 | css: { 101 | files: [ 102 | 'assets/css/*.css', 103 | '!assets/css/*.min.css' 104 | ], 105 | tasks: ['cssmin'] 106 | }, 107 | templates: { 108 | files: 'assets/templates/*.mustache', 109 | tasks: ['templates'] 110 | } 111 | }, 112 | embedImage: { 113 | all: [ 114 | 'assets/css/*.css' 115 | ] 116 | }, 117 | connect: { 118 | server: { 119 | options: { 120 | keepalive: true, 121 | hostname: '*', 122 | debug: true 123 | } 124 | } 125 | }, 126 | concurrent: { 127 | server: { 128 | tasks: ['watch', 'connect'], 129 | options: { 130 | logConcurrentOutput: true 131 | } 132 | } 133 | } 134 | }); 135 | 136 | grunt.loadTasks('tasks'); 137 | grunt.loadNpmTasks('grunt-contrib-uglify'); 138 | grunt.loadNpmTasks('grunt-contrib-watch'); 139 | grunt.loadNpmTasks('grunt-concurrent'); 140 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 141 | 142 | // Configurable port number 143 | var port = grunt.option('port'); 144 | if (port) grunt.config('connect.server.options.port', port); 145 | grunt.loadNpmTasks('grunt-contrib-connect'); 146 | grunt.registerTask('server', 'concurrent:server'); 147 | }; 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HackerWeb 2 | ========= 3 | 4 | A simply readable Hacker News web app. 5 | 6 | About 7 | ----- 8 | 9 | This project started as one of my silly mini-projects. I create this initially to try out iOS 5+ Mobile Safari's new `-webkit-overflow-scrolling: touch` CSS support. I need some sort of content for scrolling, so why not [Hacker News](https://news.ycombinator.com/)' stories? I'm also trying something called [Fake it 'til you make it](http://snook.ca/archives/conferences/fake-it) which I make the web app looks (and feels) like a native mobile app. In this case, like a native iOS app. If the web app is loaded on non-iOS devices, it'll switch to the 'web' theme which is like a generic theme for other browsers and platforms. 10 | 11 | Read my two-part blog post on how I built this web app: 12 | 13 | - [How I built the Hacker News mobile web app](http://cheeaun.com/blog/2012/03/how-i-built-hacker-news-mobile-web-app) ([HN thread](https://news.ycombinator.com/item?id=3662709)) 14 | - [How I built the Hacker News mobile web app, Part 2](http://cheeaun.com/blog/2012/03/how-i-built-hacker-news-mobile-web-app_26) ([HN thread](https://news.ycombinator.com/item?id=3756771)) 15 | 16 | Also read my introductory post, [Introducing HackerWeb](http://cheeaun.com/blog/2012/12/introducing-hackerweb). 17 | 18 | Technical stuff 19 | --------------- 20 | 21 | This web app works best on iOS 5+ Mobile Safari (iOS theme) and other modern browsers (web theme). It uses these wonderful scripts: 22 | 23 | - [Hogan.js](https://github.com/twitter/hogan.js) - logic-less templating 24 | - [Amplify.Store](http://amplifyjs.com/api/store/) - client-side storage 25 | - ruto.js - `location.hash` router 26 | - iOS 27 | - [Tappable](https://github.com/cheeaun/tappable) - touch-friendly tap events 28 | - [Tween.js](https://github.com/sole/tween.js) - simple tweening engine 29 | - Web 30 | - ibento.js - simple event delegation 31 | - [classList.js](https://github.com/eligrey/classList.js) - shim for `element.classList` 32 | - Vanilla JavaScript - everything else 33 | 34 | Also uses the [unofficial Hacker News API](https://github.com/cheeaun/node-hnapi/). 35 | 36 | Development stuff 37 | -------------------- 38 | 39 | ### Prerequisites 40 | 41 | ``` 42 | git clone git://github.com/cheeaun/hackerweb.git 43 | cd hackerweb/ 44 | npm install 45 | ``` 46 | 47 | ### [Grunt](http://gruntjs.com/) tasks 48 | 49 | - `grunt templates` - Compile templates in `templates/*` to generate `assets/js/templates.js` 50 | - `grunt uglify` - Concat and minify JavaScript files in `assets/js/*` to generate `js/*` 51 | - `grunt watch` - Watch the templates and scripts, run `templates` and `uglify` tasks when they're changed 52 | - `grunt embedImage` - Embed images into CSS files. This will parse CSS files in `assets/css/*` and change this (any lines with `url()`): 53 | 54 | ``` 55 | background-image: url(PATH); /* embedImages:url(PATH) */ 56 | ``` 57 | 58 | ... into this: 59 | 60 | ``` 61 | background-image: url(data:DATAURI); /* embedImages:url(PATH) */ 62 | ``` 63 | 64 | - `grunt connect` - Run a local dev server. Arguments: 65 | - `--port=XX` - specify a custom port number 66 | 67 | - `grunt server` - Run both `watch` and `connect` tasks at the same time 68 | 69 | Contributing and Feedback 70 | ------------------------- 71 | 72 | Feel free to fork, file some issues or [tweet your feedback](https://twitter.com/cheeaun) to me. 73 | 74 | Do check out these awesome contributions as well: 75 | 76 | - [Bookmarklet to switch between HackerNews and HackerWeb](https://gist.github.com/duncansmart/4672084) by [duncansmart](https://github.com/duncansmart) 77 | - [HNmobile Bookmarklet](http://neocoder.github.com/hnmbookmarklet/) by [neocoder](https://github.com/neocoder) 78 | - [Hacker News Menu Tab](http://www.guidefreitas.com/2012/03/hacker-news-menu-tab.html) ([GitHub](https://github.com/guidefreitas/HNewsTab)) by Guilherme Defreitas 79 | 80 | License 81 | ------- 82 | 83 | [MIT](http://cheeaun.mit-license.org/). 84 | 85 | Other similar apps 86 | ------------------ 87 | 88 | This is the not the first third-party app for Hacker News. Others have tried doing the same thing, despite some slight differences. I've compiled [a list of apps here](https://github.com/cheeaun/awesome-hacker-news). 89 | -------------------------------------------------------------------------------- /assets/css/hw-web.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif; 3 | font-size: 16px; 4 | margin: 0; 5 | padding: 0; 6 | background-color: #fff; 7 | position: relative; 8 | word-wrap: break-word; 9 | -ms-text-size-adjust: none; 10 | overflow-y: scroll; 11 | visibility: visible; 12 | } 13 | a:link{ 14 | color: #007aff; 15 | } 16 | button{ 17 | font-family: inherit; 18 | outline: 0; 19 | background-image: none; /* Override FxOS's gradient bg */ 20 | } 21 | pre{ 22 | word-wrap: normal; 23 | } 24 | pre, code{ 25 | font-family: Menlo, Monaco, Inconsolata, Consolas, "Lucida Console", monospace; 26 | font-size: 12px; 27 | } 28 | 29 | #outdated-browser{ 30 | background-color: #fff; 31 | text-align: center; 32 | padding: 1em; 33 | margin: 0; 34 | } 35 | #outdated-browser p{ 36 | margin: 0; 37 | padding: 0; 38 | line-height: 1.5em; 39 | } 40 | 41 | .icon-refresh{ 42 | display: inline-block; 43 | width: 14px; 44 | height: 18px; 45 | background: transparent url(../images/web/refresh-icon.svg) no-repeat; 46 | background-size: 14px 18px; 47 | vertical-align: middle; 48 | overflow: hidden; 49 | text-indent: -999px; 50 | } 51 | 52 | .view{ 53 | position: relative; 54 | margin: 0 auto; 55 | padding-bottom: 1px; 56 | overflow: hidden; 57 | } 58 | .view.hidden{ 59 | display: none; 60 | } 61 | .view>header{ 62 | border-bottom: 1px solid #ddd; 63 | background-color: rgba(255,255,255,.95); 64 | line-height: 44px; 65 | height: 44px; 66 | color: #333; 67 | position: absolute; /* if position:fixed is not supported */ 68 | position: fixed; 69 | left: 0; 70 | top: 0; 71 | right: 0; 72 | z-index: 3; 73 | } 74 | .header-button{ 75 | position: absolute; 76 | top: 0; 77 | padding: 7px 5px; 78 | line-height: 1em; 79 | cursor: pointer; 80 | } 81 | .header-button button{ 82 | pointer-events: none; 83 | min-width: 50px; 84 | height: 30px; 85 | line-height: 25px; 86 | font-weight: normal; 87 | font-size: 15px; 88 | text-align: center; 89 | color: #f60; 90 | padding: 0 4px; 91 | margin: 0; 92 | text-decoration: none; 93 | border: 0; 94 | background-color: transparent; 95 | cursor: pointer; 96 | } 97 | .header-button:hover{ 98 | background-color: #F6F6EF; 99 | } 100 | .header-button:active button{ 101 | opacity: .5; 102 | } 103 | .header-button button::-moz-focus-inner{ 104 | padding: 0; 105 | border: 0; 106 | } 107 | .header-button-icon button{ 108 | min-width: 0; 109 | padding: 0 8px; 110 | } 111 | .header-button-icon button img{ 112 | vertical-align: text-bottom; 113 | } 114 | .header-back-button button{ 115 | } 116 | .header-button-left{ 117 | left: 0; 118 | } 119 | .header-button-right{ 120 | right: 0; 121 | } 122 | 123 | #view-comments .header-button-left button, 124 | #view-about .header-button-left button{ 125 | overflow: hidden; 126 | text-indent: -999px; 127 | min-width: 32px; 128 | background-image: url(../images/web/home-icon.svg); 129 | background-size: 19.5px 17px; 130 | background-repeat: no-repeat; 131 | background-position: center; 132 | } 133 | 134 | .view>header h1{ 135 | cursor: default; 136 | padding: 0; 137 | text-align: center; 138 | font-size: 1em; 139 | font-weight: normal; 140 | margin: 0; 141 | white-space: nowrap; 142 | text-overflow: ellipsis; 143 | overflow: hidden; 144 | word-wrap: normal; 145 | } 146 | .view>.scroll{ 147 | overflow: hidden; 148 | min-height: 9vh; 149 | margin-top: 44px; 150 | } 151 | .view.shaded>.scroll{ 152 | background-color: #F6F6EF; 153 | } 154 | .view.shaded>.scroll p.foot-label{ 155 | margin: 15px 10px; 156 | font-size: 13px; 157 | line-height: 1.4em; 158 | color: #4c566c; 159 | text-align: center; 160 | } 161 | .view>.scroll .loader, 162 | .view>.scroll .load-error{ 163 | font-size: 14px; 164 | color: #666; 165 | text-align: center; 166 | padding: 50px 0; 167 | } 168 | .view>.scroll .load-error button{ 169 | border: 0; 170 | height: 30px; 171 | line-height: 30px; 172 | cursor: pointer; 173 | padding: 0 20px; 174 | font-weight: 500; 175 | color: #666; 176 | margin: 6px auto 4px; 177 | background-color: #f0f0ea; 178 | border-radius: 2px; 179 | } 180 | .view>.scroll .load-error button:hover{ 181 | background-color: #e0e0da; 182 | } 183 | .view>.scroll .load-error button:active{ 184 | background-color: #c8c8c8; 185 | } 186 | .tableview{ 187 | margin: 0; 188 | padding: 0; 189 | list-style: none; 190 | background-color: #fff; 191 | } 192 | .tableview li{ 193 | display: block; 194 | border-top: 1px solid #e0e0e0; 195 | padding: 10px; 196 | } 197 | .tableview li:first-child{ 198 | border-top: 0; 199 | } 200 | .tableview-links li{ 201 | padding: 0; 202 | position: relative; 203 | } 204 | .tableview-links li>a{ 205 | line-height: 1.2em; 206 | color: #000; 207 | display: block; 208 | padding: 10px 5px; 209 | text-decoration: none; 210 | } 211 | .tableview-links li>a:visited{ 212 | color: #888; 213 | } 214 | .tableview-links li>a:hover{ 215 | background-color: #F6F6EF; 216 | } 217 | .tableview-links li>a *{ 218 | pointer-events: none; 219 | } 220 | .tableview-links li>a .number{ 221 | font-weight: lighter; 222 | text-align: right; 223 | width: 3.5ex; 224 | color: #666d74; 225 | position: absolute; 226 | left: 5px; 227 | top: 10px; 228 | } 229 | .tableview-links li>a .story{ 230 | margin-left: 4ex; 231 | padding-left: 5px; 232 | } 233 | .tableview-links li>a .story b{ 234 | font-weight: normal; 235 | } 236 | .tableview-links li>a .metadata{ 237 | display: block; 238 | font-size: 14px; 239 | color: #666d74; 240 | } 241 | .tableview-links li>a:hover .link-text{ 242 | text-decoration: underline; 243 | } 244 | .tableview-links li>a.detail-disclosure{ 245 | padding-right: 44px; 246 | } 247 | .tableview-links li>a.detail-disclosure-button{ 248 | position: absolute; 249 | top: 0; 250 | right: 0; 251 | padding: 0; 252 | z-index: 2; 253 | width: 44px; 254 | height: 100%; 255 | text-align: center; 256 | vertical-align: middle; 257 | } 258 | .tableview-links li>a.detail-disclosure-button span{ 259 | display: inline-block; 260 | width: 17px; 261 | height: 17px; 262 | background: transparent url(../images/web/comments-icon.svg) no-repeat; 263 | background-size: 17px 17px; 264 | margin: 15px 0; 265 | } 266 | .tableview-links li>a.detail-disclosure-button:active span{ 267 | opacity: .5; 268 | } 269 | .tableview-links li>a.more-link{ 270 | color: #007aff; 271 | cursor: pointer; 272 | line-height: 3em; 273 | padding: 10px; 274 | display: block; 275 | text-align: center; 276 | } 277 | .tableview-links li>a.more-link:active{ 278 | opacity: .5; 279 | } 280 | 281 | .grouped-tableview{ 282 | margin: 10px 9px 11px; 283 | padding: 10px; 284 | border: 1px solid #E0E0E0; 285 | background-color: #fff; 286 | border-radius: 2px; 287 | overflow: hidden; 288 | } 289 | .grouped-tableview + .grouped-tableview{ 290 | margin-top: 0; 291 | } 292 | .grouped-tableview p{ 293 | margin: 0 0 1em; 294 | } 295 | .grouped-tableview p:last-child{ 296 | margin: 0; 297 | } 298 | .grouped-tableview ul, 299 | .grouped-tableview ol{ 300 | margin: 0 0 0 2em; 301 | padding: 0; 302 | line-height: 1.5em; 303 | } 304 | ul.grouped-tableview, 305 | ol.grouped-tableview{ 306 | display: block; 307 | list-style: none; 308 | padding: 0; 309 | } 310 | ul.grouped-tableview li, 311 | ol.grouped-tableview li{ 312 | display: block; 313 | border-bottom: 1px solid #E0E0E0; 314 | padding: 10px; 315 | } 316 | ul.grouped-tableview li:first-child, 317 | ol.grouped-tableview li:first-child{ 318 | box-shadow: none; 319 | } 320 | ul.grouped-tableview li:last-child, 321 | ol.grouped-tableview li:last-child{ 322 | border-bottom: 0; 323 | } 324 | ul.grouped-tableview-links li, 325 | ol.grouped-tableview-links li{ 326 | padding: 0; 327 | } 328 | ul.grouped-tableview-links li>a, 329 | ol.grouped-tableview-links li>a{ 330 | color: #000; 331 | display: inline-block; 332 | box-sizing: border-box; 333 | width: 100%; 334 | padding: 10px; 335 | text-decoration: none; 336 | white-space: nowrap; 337 | text-overflow: ellipsis; 338 | line-height: 1.2em; 339 | } 340 | ul.grouped-tableview-links li>a:hover, 341 | ol.grouped-tableview-links li>a:hover{ 342 | background-color: #F6F6EF; 343 | } 344 | 345 | .view .post-content{ 346 | min-height: 4em; 347 | overflow: hidden; 348 | } 349 | .view .post-content header{ 350 | padding: 10px 15px; 351 | line-height: 1.2em; 352 | } 353 | .view .post-content header h1, 354 | .view .post-content header p{ 355 | font-size: 1em; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | .view .post-content header a{ 360 | display: block; 361 | text-decoration: none; 362 | } 363 | .view .post-content header h1{ 364 | color: #000; 365 | font-weight: normal; 366 | font-size: 1em; 367 | } 368 | .view .post-content header .link-text{ 369 | font-size: 14px; 370 | } 371 | .view .post-content header .metadata{ 372 | font-size: 14px; 373 | color: #666d74; 374 | } 375 | .view .post-content header .metadata a.external-link{ 376 | color: #666d74; 377 | text-decoration: none; 378 | display: block; 379 | overflow: hidden; 380 | white-space: nowrap; 381 | text-overflow: ellipsis; 382 | padding-left: 16px; 383 | background: transparent url(../images/web/external-arrow.svg) no-repeat 0 50%; 384 | background-size: 12px 10px; 385 | } 386 | .view .post-content header .metadata a.external-link:hover{ 387 | color: #007aff; 388 | } 389 | .view .post-content header + .grouped-tableview{ 390 | margin-top: 0; 391 | } 392 | 393 | .view .post-content .poll{ 394 | margin: 0; 395 | list-style: none; 396 | } 397 | .view .post-content .poll li{ 398 | margin-bottom: 9px; 399 | } 400 | .view .post-content .poll li .poll-details{ 401 | display: table; 402 | width: 100%; 403 | } 404 | .view .post-content .poll li .poll-details b{ 405 | font-weight: normal; 406 | display: table-cell; 407 | color: #333; 408 | } 409 | .view .post-content .poll li .poll-details .points{ 410 | display: table-cell; 411 | white-space: nowrap; 412 | color: #888; 413 | text-align: right; 414 | vertical-align: bottom; 415 | } 416 | .view .post-content .poll li .poll-bar{ 417 | background-color: #fafafa; 418 | width: 100%; 419 | height: 3px; 420 | overflow: hidden; 421 | border-radius: 2px; 422 | } 423 | .view .post-content .poll li .poll-bar span{ 424 | background-color: #007aff; 425 | display: block; 426 | width: 0; 427 | height: 3px; 428 | } 429 | .view .post-content pre, 430 | .view section.comments pre{ 431 | margin: 0 0 8px; 432 | overflow: auto; 433 | padding: .65em 0; 434 | background-color: #F6F6EF; 435 | border-radius: 2px; 436 | } 437 | 438 | .view section.comments{ 439 | font-size: 14px; 440 | background-color: #fff; 441 | border-top: 1px solid #E0E0E0; 442 | border-bottom: 1px solid #E0E0E0; 443 | margin-bottom: 20px; 444 | } 445 | .view section.comments ul{ 446 | margin: 0; 447 | padding: 0; 448 | list-style: none; 449 | line-height: 1.5em; 450 | } 451 | .view section.comments ul li{ 452 | padding: 15px; 453 | border-bottom: 1px solid #E0E0E0; 454 | } 455 | .view section.comments ul>li:last-child{ 456 | border-bottom: 0; 457 | } 458 | .view section.comments ul ul li{ 459 | padding: 0 0 0 15px; 460 | border: 0; 461 | background: transparent url(../images/web/comment-arrow.svg) no-repeat 0 4px; 462 | background-size: 10px 9px; 463 | color: #333; 464 | } 465 | .view section.comments p{ 466 | margin: 8px 0; 467 | } 468 | .view section.comments p.metadata{ 469 | color: #666d74; 470 | padding: 0; 471 | margin: 0; 472 | } 473 | .view section.comments p.metadata .user{ 474 | font-weight: normal; 475 | float: left; 476 | color: #bf223f; 477 | } 478 | .view section.comments p.metadata.deleted{ 479 | margin-bottom: 8px; 480 | } 481 | .view section.comments p.metadata.deleted span{ 482 | float: left; 483 | } 484 | .view section.comments p.metadata .user.op:after{ 485 | content: 'OP'; 486 | display: inline-block; 487 | font-size: 70%; 488 | color: #fff; 489 | background-color: #bf223f; 490 | padding: 3px 4px; 491 | line-height: 1; 492 | margin-left: 5px; 493 | border-radius: 2px; 494 | } 495 | .view section.comments p.metadata time{ 496 | display: block; 497 | text-align: right; 498 | color: #888; 499 | } 500 | .view section.comments p.metadata time a{ 501 | color: #888; 502 | text-decoration: none; 503 | } 504 | .view section.comments p.metadata time a:hover{ 505 | text-decoration: underline; 506 | } 507 | .view section.comments p.no-comments{ 508 | text-align: center; 509 | margin: 50px 0; 510 | color: #666; 511 | } 512 | .view section.comments button.comments-toggle{ 513 | display: block; 514 | height: 30px; 515 | line-height: 22px; 516 | cursor: pointer; 517 | padding: 0 13px; 518 | font-size: 1em; 519 | color: #666; 520 | margin: 2px 0 6px; 521 | background-color: #f0f0ea; 522 | border: 0; 523 | -webkit-user-select: none; 524 | border-radius: 2px; 525 | } 526 | .view section.comments button.comments-toggle:hover{ 527 | background-color: #e0e0da; 528 | } 529 | .view section.comments button.comments-toggle:active{ 530 | background-color: #c8c8c8; 531 | } 532 | .view section.comments li.more-link-container{ 533 | padding: 0; 534 | } 535 | 536 | .link-text{ 537 | color: #003d80; 538 | font-weight: normal; 539 | } 540 | .inline-block{ 541 | display: inline-block; 542 | } 543 | 544 | /* For modern browsers */ 545 | .cf:before, 546 | .cf:after { 547 | content:""; 548 | display:table; 549 | } 550 | 551 | .cf:after { 552 | clear:both; 553 | } 554 | 555 | #y-icon{ 556 | background: transparent url(../../icons/icon.svg) no-repeat; 557 | background-size: 57px 57px; 558 | width: 57px; 559 | height: 57px; 560 | float: left; 561 | border: 1px solid #eee; 562 | border-radius: 4px; 563 | } 564 | #app-desc{ 565 | margin-left: 67px; 566 | font-size: 14px; 567 | color: #666d74; 568 | } 569 | #app-desc strong{ 570 | color: #000; 571 | font-size: 1rem; 572 | font-weight: normal; 573 | } 574 | 575 | @-ms-viewport{ 576 | width: device-width; 577 | zoom: 1.0; 578 | } 579 | 580 | @viewport{ 581 | width: device-width; 582 | zoom: 1.0; 583 | } 584 | 585 | @media only screen and (min-width: 788px){ 586 | body{ 587 | background-color: #fafafa; 588 | } 589 | .view{ 590 | background-color: #fff; 591 | max-width: 700px; 592 | box-shadow: 0 0 1px #aaa; 593 | } 594 | .view>header{ 595 | position: static; 596 | } 597 | .view>.scroll{ 598 | margin-top: 0; 599 | } 600 | } 601 | 602 | @media (prefers-color-scheme: dark){ 603 | body{ 604 | color: rgba(255,255,255,.87); 605 | background-color: #121212; 606 | } 607 | 608 | a:link{ 609 | color: #2988cf; 610 | } 611 | a:visited{ 612 | color: #c06dc8; 613 | } 614 | 615 | /* Home */ 616 | .tableview{ 617 | background-color: #121212; 618 | } 619 | .tableview-links li>a{ 620 | color: rgba(255,255,255,.87); 621 | } 622 | .tableview-links li>a:hover{ 623 | background-color: rgba(255,255,255,.05); 624 | } 625 | .tableview-links li>a:visited{ 626 | opacity: .38; 627 | } 628 | .tableview-links li>a .metadata, 629 | .tableview-links li>a .number{ 630 | color: rgba(255,255,255,.6); 631 | } 632 | .tableview li, 633 | .view>header{ 634 | border-color: rgba(255,255,255,.05); 635 | } 636 | 637 | /* Post */ 638 | .view>header{ 639 | background-color: #1e1e1e; 640 | } 641 | .view section.comments, 642 | .header-button:hover{ 643 | background-color: rgba(255,255,255,.05); 644 | } 645 | .view.shaded>.scroll{ 646 | background-color: transparent; 647 | } 648 | .view>header h1, 649 | .view .post-content header h1{ 650 | color: rgba(255,255,255,.87); 651 | } 652 | .view section.comments, 653 | .view section.comments ul li{ 654 | border-color: rgba(255,255,255,.05); 655 | } 656 | .view section.comments p.metadata .user{ 657 | color: #e84f66; 658 | } 659 | .view section.comments p.metadata .user.op:after{ 660 | background-color: #e84f66; 661 | } 662 | .link-text, 663 | .tableview-links li>a.more-link, 664 | .view .post-content header .metadata a.external-link:hover{ 665 | color: #2988cf; 666 | } 667 | .view .post-content .poll li .poll-bar span{ 668 | background-color: #2988cf; 669 | } 670 | .view section.comments button.comments-toggle{ 671 | background-color: #333; 672 | color: #eee 673 | } 674 | .view section.comments button.comments-toggle:active{ 675 | background-color: #555; 676 | } 677 | .view section.comments button.comments-toggle:hover{ 678 | background-color: #444; 679 | } 680 | .header-button button{ 681 | color: #ff8700; 682 | } 683 | .icon-refresh, 684 | #view-comments .header-button-left button, 685 | #view-about .header-button-left button, 686 | .tableview-links li>a.detail-disclosure-button span{ 687 | filter: brightness(1.3); 688 | } 689 | .view .post-content pre, 690 | .view section.comments pre{ 691 | background-color: rgba(255,255,255,.05); 692 | } 693 | .view section.comments ul ul li{ 694 | color: rgba(255,255,255,.6); 695 | } 696 | 697 | /* About */ 698 | .grouped-tableview{ 699 | background-color: rgba(255,255,255,.05); 700 | } 701 | #app-desc strong, 702 | ol.grouped-tableview-links li>a, 703 | ul.grouped-tableview-links li>a{ 704 | color: rgba(255,255,255,.87); 705 | } 706 | .grouped-tableview, 707 | ol.grouped-tableview li, 708 | ul.grouped-tableview li{ 709 | border-color: rgba(255,255,255,.05); 710 | } 711 | .view.shaded>.scroll p.foot-label, 712 | .grouped-tableview p{ 713 | color: rgba(255,255,255,.6); 714 | } 715 | ol.grouped-tableview-links li>a:hover, 716 | ul.grouped-tableview-links li>a:hover{ 717 | background-color: rgba(255,255,255,.05); 718 | } 719 | 720 | @media only screen and (min-width: 788px){ 721 | .view{ 722 | background-color: #121212; 723 | } 724 | } 725 | } 726 | -------------------------------------------------------------------------------- /assets/css/hw-web.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:16px;margin:0;padding:0;background-color:#fff;position:relative;word-wrap:break-word;-ms-text-size-adjust:none;overflow-y:scroll;visibility:visible}a:link{color:#007aff}button{font-family:inherit;outline:0;background-image:none}pre{word-wrap:normal}code,pre{font-family:Menlo,Monaco,Inconsolata,Consolas,"Lucida Console",monospace;font-size:12px}#outdated-browser{background-color:#fff;text-align:center;padding:1em;margin:0}#outdated-browser p{margin:0;padding:0;line-height:1.5em}.icon-refresh{display:inline-block;width:14px;height:18px;background:transparent url(../images/web/refresh-icon.svg) no-repeat;background-size:14px 18px;vertical-align:middle;overflow:hidden;text-indent:-999px}.view{position:relative;margin:0 auto;padding-bottom:1px;overflow:hidden}.view.hidden{display:none}.view>header{border-bottom:1px solid #ddd;background-color:rgba(255,255,255,.95);line-height:44px;height:44px;color:#333;position:absolute;position:fixed;left:0;top:0;right:0;z-index:3}.header-button{position:absolute;top:0;padding:7px 5px;line-height:1em;cursor:pointer}.header-button button{pointer-events:none;min-width:50px;height:30px;line-height:25px;font-weight:400;font-size:15px;text-align:center;color:#f60;padding:0 4px;margin:0;text-decoration:none;border:0;background-color:transparent;cursor:pointer}.header-button:hover{background-color:#f6f6ef}.header-button:active button{opacity:.5}.header-button button::-moz-focus-inner{padding:0;border:0}.header-button-icon button{min-width:0;padding:0 8px}.header-button-icon button img{vertical-align:text-bottom}.header-button-left{left:0}.header-button-right{right:0}#view-about .header-button-left button,#view-comments .header-button-left button{overflow:hidden;text-indent:-999px;min-width:32px;background-image:url(../images/web/home-icon.svg);background-size:19.5px 17px;background-repeat:no-repeat;background-position:center}.view>header h1{cursor:default;padding:0;text-align:center;font-size:1em;font-weight:400;margin:0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;word-wrap:normal}.view>.scroll{overflow:hidden;min-height:9vh;margin-top:44px}.view.shaded>.scroll{background-color:#f6f6ef}.view.shaded>.scroll p.foot-label{margin:15px 10px;font-size:13px;line-height:1.4em;color:#4c566c;text-align:center}.view>.scroll .load-error,.view>.scroll .loader{font-size:14px;color:#666;text-align:center;padding:50px 0}.view>.scroll .load-error button{border:0;height:30px;line-height:30px;cursor:pointer;padding:0 20px;font-weight:500;color:#666;margin:6px auto 4px;background-color:#f0f0ea;border-radius:2px}.view>.scroll .load-error button:hover{background-color:#e0e0da}.view>.scroll .load-error button:active{background-color:#c8c8c8}.tableview{margin:0;padding:0;list-style:none;background-color:#fff}.tableview li{display:block;border-top:1px solid #e0e0e0;padding:10px}.tableview li:first-child{border-top:0}.tableview-links li{padding:0;position:relative}.tableview-links li>a{line-height:1.2em;color:#000;display:block;padding:10px 5px;text-decoration:none}.tableview-links li>a:visited{color:#888}.tableview-links li>a:hover{background-color:#f6f6ef}.tableview-links li>a *{pointer-events:none}.tableview-links li>a .number{font-weight:lighter;text-align:right;width:3.5ex;color:#666d74;position:absolute;left:5px;top:10px}.tableview-links li>a .story{margin-left:4ex;padding-left:5px}.tableview-links li>a .story b{font-weight:400}.tableview-links li>a .metadata{display:block;font-size:14px;color:#666d74}.tableview-links li>a:hover .link-text{text-decoration:underline}.tableview-links li>a.detail-disclosure{padding-right:44px}.tableview-links li>a.detail-disclosure-button{position:absolute;top:0;right:0;padding:0;z-index:2;width:44px;height:100%;text-align:center;vertical-align:middle}.tableview-links li>a.detail-disclosure-button span{display:inline-block;width:17px;height:17px;background:transparent url(../images/web/comments-icon.svg) no-repeat;background-size:17px 17px;margin:15px 0}.tableview-links li>a.detail-disclosure-button:active span{opacity:.5}.tableview-links li>a.more-link{color:#007aff;cursor:pointer;line-height:3em;padding:10px;display:block;text-align:center}.tableview-links li>a.more-link:active{opacity:.5}.grouped-tableview{margin:10px 9px 11px;padding:10px;border:1px solid #e0e0e0;background-color:#fff;border-radius:2px;overflow:hidden}.grouped-tableview+.grouped-tableview{margin-top:0}.grouped-tableview p{margin:0 0 1em}.grouped-tableview p:last-child{margin:0}.grouped-tableview ol,.grouped-tableview ul{margin:0 0 0 2em;padding:0;line-height:1.5em}ol.grouped-tableview,ul.grouped-tableview{display:block;list-style:none;padding:0}ol.grouped-tableview li,ul.grouped-tableview li{display:block;border-bottom:1px solid #e0e0e0;padding:10px}ol.grouped-tableview li:first-child,ul.grouped-tableview li:first-child{box-shadow:none}ol.grouped-tableview li:last-child,ul.grouped-tableview li:last-child{border-bottom:0}ol.grouped-tableview-links li,ul.grouped-tableview-links li{padding:0}ol.grouped-tableview-links li>a,ul.grouped-tableview-links li>a{color:#000;display:inline-block;box-sizing:border-box;width:100%;padding:10px;text-decoration:none;white-space:nowrap;text-overflow:ellipsis;line-height:1.2em}ol.grouped-tableview-links li>a:hover,ul.grouped-tableview-links li>a:hover{background-color:#f6f6ef}.view .post-content{min-height:4em;overflow:hidden}.view .post-content header{padding:10px 15px;line-height:1.2em}.view .post-content header h1,.view .post-content header p{font-size:1em;margin:0;padding:0}.view .post-content header a{display:block;text-decoration:none}.view .post-content header h1{color:#000;font-weight:400;font-size:1em}.view .post-content header .link-text{font-size:14px}.view .post-content header .metadata{font-size:14px;color:#666d74}.view .post-content header .metadata a.external-link{color:#666d74;text-decoration:none;display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding-left:16px;background:transparent url(../images/web/external-arrow.svg) no-repeat 0 50%;background-size:12px 10px}.view .post-content header .metadata a.external-link:hover{color:#007aff}.view .post-content header+.grouped-tableview{margin-top:0}.view .post-content .poll{margin:0;list-style:none}.view .post-content .poll li{margin-bottom:9px}.view .post-content .poll li .poll-details{display:table;width:100%}.view .post-content .poll li .poll-details b{font-weight:400;display:table-cell;color:#333}.view .post-content .poll li .poll-details .points{display:table-cell;white-space:nowrap;color:#888;text-align:right;vertical-align:bottom}.view .post-content .poll li .poll-bar{background-color:#fafafa;width:100%;height:3px;overflow:hidden;border-radius:2px}.view .post-content .poll li .poll-bar span{background-color:#007aff;display:block;width:0;height:3px}.view .post-content pre,.view section.comments pre{margin:0 0 8px;overflow:auto;padding:.65em 0;background-color:#f6f6ef;border-radius:2px}.view section.comments{font-size:14px;background-color:#fff;border-top:1px solid #e0e0e0;border-bottom:1px solid #e0e0e0;margin-bottom:20px}.view section.comments ul{margin:0;padding:0;list-style:none;line-height:1.5em}.view section.comments ul li{padding:15px;border-bottom:1px solid #e0e0e0}.view section.comments ul>li:last-child{border-bottom:0}.view section.comments ul ul li{padding:0 0 0 15px;border:0;background:transparent url(../images/web/comment-arrow.svg) no-repeat 0 4px;background-size:10px 9px;color:#333}.view section.comments p{margin:8px 0}.view section.comments p.metadata{color:#666d74;padding:0;margin:0}.view section.comments p.metadata .user{font-weight:400;float:left;color:#bf223f}.view section.comments p.metadata.deleted{margin-bottom:8px}.view section.comments p.metadata.deleted span{float:left}.view section.comments p.metadata .user.op:after{content:'OP';display:inline-block;font-size:70%;color:#fff;background-color:#bf223f;padding:3px 4px;line-height:1;margin-left:5px;border-radius:2px}.view section.comments p.metadata time{display:block;text-align:right;color:#888}.view section.comments p.metadata time a{color:#888;text-decoration:none}.view section.comments p.metadata time a:hover{text-decoration:underline}.view section.comments p.no-comments{text-align:center;margin:50px 0;color:#666}.view section.comments button.comments-toggle{display:block;height:30px;line-height:22px;cursor:pointer;padding:0 13px;font-size:1em;color:#666;margin:2px 0 6px;background-color:#f0f0ea;border:0;-webkit-user-select:none;border-radius:2px}.view section.comments button.comments-toggle:hover{background-color:#e0e0da}.view section.comments button.comments-toggle:active{background-color:#c8c8c8}.view section.comments li.more-link-container{padding:0}.link-text{color:#003d80;font-weight:400}.inline-block{display:inline-block}.cf:after,.cf:before{content:"";display:table}.cf:after{clear:both}#y-icon{background:transparent url(../../icons/icon.svg) no-repeat;background-size:57px 57px;width:57px;height:57px;float:left;border:1px solid #eee;border-radius:4px}#app-desc{margin-left:67px;font-size:14px;color:#666d74}#app-desc strong{color:#000;font-size:1rem;font-weight:400}@-ms-viewport{width:device-width;zoom:1}@viewport{width:device-width;zoom:1}@media only screen and (min-width:788px){body{background-color:#fafafa}.view{background-color:#fff;max-width:700px;box-shadow:0 0 1px #aaa}.view>header{position:static}.view>.scroll{margin-top:0}}@media (prefers-color-scheme:dark){body{color:rgba(255,255,255,.87);background-color:#121212}a:link{color:#2988cf}a:visited{color:#c06dc8}.tableview{background-color:#121212}.tableview-links li>a{color:rgba(255,255,255,.87)}.tableview-links li>a:hover{background-color:rgba(255,255,255,.05)}.tableview-links li>a:visited{opacity:.38}.tableview-links li>a .metadata,.tableview-links li>a .number{color:rgba(255,255,255,.6)}.tableview li,.view>header{border-color:rgba(255,255,255,.05)}.view>header{background-color:#1e1e1e}.header-button:hover,.view section.comments{background-color:rgba(255,255,255,.05)}.view.shaded>.scroll{background-color:transparent}.view .post-content header h1,.view>header h1{color:rgba(255,255,255,.87)}.view section.comments,.view section.comments ul li{border-color:rgba(255,255,255,.05)}.view section.comments p.metadata .user{color:#e84f66}.view section.comments p.metadata .user.op:after{background-color:#e84f66}.link-text,.tableview-links li>a.more-link,.view .post-content header .metadata a.external-link:hover{color:#2988cf}.view .post-content .poll li .poll-bar span{background-color:#2988cf}.view section.comments button.comments-toggle{background-color:#333;color:#eee}.view section.comments button.comments-toggle:active{background-color:#555}.view section.comments button.comments-toggle:hover{background-color:#444}.header-button button{color:#ff8700}#view-about .header-button-left button,#view-comments .header-button-left button,.icon-refresh,.tableview-links li>a.detail-disclosure-button span{filter:brightness(1.3)}.view .post-content pre,.view section.comments pre{background-color:rgba(255,255,255,.05)}.view section.comments ul ul li{color:rgba(255,255,255,.6)}.grouped-tableview{background-color:rgba(255,255,255,.05)}#app-desc strong,ol.grouped-tableview-links li>a,ul.grouped-tableview-links li>a{color:rgba(255,255,255,.87)}.grouped-tableview,ol.grouped-tableview li,ul.grouped-tableview li{border-color:rgba(255,255,255,.05)}.grouped-tableview p,.view.shaded>.scroll p.foot-label{color:rgba(255,255,255,.6)}ol.grouped-tableview-links li>a:hover,ul.grouped-tableview-links li>a:hover{background-color:rgba(255,255,255,.05)}@media only screen and (min-width:788px){.view{background-color:#121212}}} -------------------------------------------------------------------------------- /assets/images/comment-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/comment-arrow.png -------------------------------------------------------------------------------- /assets/images/external-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/external-arrow.png -------------------------------------------------------------------------------- /assets/images/ios/activity-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/activity-indicator.png -------------------------------------------------------------------------------- /assets/images/ios/back-button-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/back-button-5.png -------------------------------------------------------------------------------- /assets/images/ios/back-button-active-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/back-button-active-5.png -------------------------------------------------------------------------------- /assets/images/ios/back-button-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/back-button-active.png -------------------------------------------------------------------------------- /assets/images/ios/back-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/back-button.png -------------------------------------------------------------------------------- /assets/images/ios/button-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/button-5.png -------------------------------------------------------------------------------- /assets/images/ios/button-active-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/button-active-5.png -------------------------------------------------------------------------------- /assets/images/ios/button-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/button-active.png -------------------------------------------------------------------------------- /assets/images/ios/button-bar-press-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/button-bar-press-indicator.png -------------------------------------------------------------------------------- /assets/images/ios/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/button.png -------------------------------------------------------------------------------- /assets/images/ios/close-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/close-button.png -------------------------------------------------------------------------------- /assets/images/ios/detail-disclosure-button-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/detail-disclosure-button-active.png -------------------------------------------------------------------------------- /assets/images/ios/detail-disclosure-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/detail-disclosure-button.png -------------------------------------------------------------------------------- /assets/images/ios/disclosure-indicator-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/disclosure-indicator-active.png -------------------------------------------------------------------------------- /assets/images/ios/disclosure-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/disclosure-indicator.png -------------------------------------------------------------------------------- /assets/images/ios/nav-bar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/nav-bar-5.png -------------------------------------------------------------------------------- /assets/images/ios/nav-bar-mini-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/nav-bar-mini-5.png -------------------------------------------------------------------------------- /assets/images/ios/nav-bar-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/nav-bar-mini.png -------------------------------------------------------------------------------- /assets/images/ios/nav-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/nav-bar.png -------------------------------------------------------------------------------- /assets/images/ios/refresh-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/refresh-icon.png -------------------------------------------------------------------------------- /assets/images/ios/silver-button-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/silver-button-active.png -------------------------------------------------------------------------------- /assets/images/ios/silver-button-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/silver-button-bar.png -------------------------------------------------------------------------------- /assets/images/ios/silver-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/silver-button.png -------------------------------------------------------------------------------- /assets/images/ios/silver-nav-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios/silver-nav-bar.png -------------------------------------------------------------------------------- /assets/images/ios7/back-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/back-icon.png -------------------------------------------------------------------------------- /assets/images/ios7/close-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/close-button.png -------------------------------------------------------------------------------- /assets/images/ios7/comment-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/comment-arrow.png -------------------------------------------------------------------------------- /assets/images/ios7/comment-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/comment-button.png -------------------------------------------------------------------------------- /assets/images/ios7/disclosure-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/disclosure-indicator.png -------------------------------------------------------------------------------- /assets/images/ios7/external-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/external-arrow.png -------------------------------------------------------------------------------- /assets/images/ios7/refresh-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/ios7/refresh-icon.png -------------------------------------------------------------------------------- /assets/images/web/comment-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/web/comments-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/web/comments-icon.png -------------------------------------------------------------------------------- /assets/images/web/comments-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/web/external-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/web/home-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/web/home-icon.png -------------------------------------------------------------------------------- /assets/images/web/home-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/web/refresh-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/assets/images/web/refresh-icon.png -------------------------------------------------------------------------------- /assets/images/web/refresh-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/js/hw-ios-2.js: -------------------------------------------------------------------------------- 1 | (function(w){ 2 | var d = w.document; 3 | var body = d.body; 4 | 5 | var pfx = ['webkit', 'moz', 'MS', 'o', '']; 6 | var pfxLength = pfx.length; 7 | var prefixedAddEvent = function(element, type, callback){ 8 | for (var p = 0; p < pfxLength; p++) { 9 | if (!pfx[p]) type = type.toLowerCase(); 10 | element.addEventListener(pfx[p]+type, callback, false); 11 | } 12 | }; 13 | var prefixedRemoveEvent = function(element, type, callback){ 14 | for (var p = 0; p < pfxLength; p++) { 15 | if (!pfx[p]) type = type.toLowerCase(); 16 | element.removeEventListener(pfx[p]+type, callback, false); 17 | } 18 | }; 19 | 20 | var slideWise = { 21 | // outEl, inEl 22 | rtl: ['slide-out-to-left', 'slide-in-from-right'], 23 | ltr: ['slide-out-to-right', 'slide-in-from-left'] 24 | }; 25 | var slide = function(opts){ 26 | var inEl = opts['in']; 27 | var outEl = opts.out; 28 | var inClass = inEl.classList; 29 | var outClass = outEl.classList; 30 | var direction = opts.direction; 31 | var wise = slideWise[direction]; 32 | var reset = function(){ 33 | outClass.add('hidden'); 34 | outClass.remove(wise[0]); 35 | inClass.remove(wise[1]); 36 | prefixedRemoveEvent(inEl, 'AnimationEnd', reset); 37 | }; 38 | prefixedAddEvent(inEl, 'AnimationEnd', reset); 39 | inClass.remove('hidden'); 40 | inClass.add(wise[1]); 41 | outClass.add(wise[0]); 42 | }; 43 | 44 | var pop = function(opts){ 45 | var inEl = opts['in']; 46 | var outEl = opts.out; 47 | var inClass = inEl.classList; 48 | var outClass = outEl.classList; 49 | var direction = opts.direction; 50 | if (direction == 'up'){ 51 | outClass.add('no-pointer'); 52 | inClass.remove('hidden'); 53 | inClass.add('slide-up'); 54 | } else { 55 | var resetDown = function(){ 56 | outClass.remove('slide-down'); 57 | outClass.add('hidden'); 58 | inClass.remove('no-pointer'); 59 | prefixedRemoveEvent(outEl, 'AnimationEnd', resetDown); 60 | }; 61 | prefixedAddEvent(outEl, 'AnimationEnd', resetDown); 62 | outClass.remove('slide-up'); 63 | outClass.add('slide-down'); 64 | } 65 | }; 66 | 67 | var getScreenState = function(){ 68 | return ((body.offsetWidth || w.innerWidth) > 736) ? 'wide' : 'narrow'; 69 | }; 70 | 71 | // Disable user scale of the viewport 72 | var vmeta = d.querySelector('meta[name=viewport]'); 73 | if (!vmeta){ 74 | vmeta = d.createElement('meta'); 75 | vmeta.name = 'viewport'; 76 | d.head.appendChild(vmeta); 77 | } 78 | vmeta.content = 'width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0'; 79 | 80 | // Wide screen state 81 | var isWideScreen = getScreenState() == 'wide'; 82 | w.addEventListener('resize', function(){ 83 | var wide = getScreenState() == 'wide'; 84 | if (wide != isWideScreen){ 85 | isWideScreen = wide; 86 | location.reload(); 87 | } 88 | }); 89 | 90 | // Inject some elements for additional iOS decorations 91 | if (isWideScreen) body.insertAdjacentHTML('beforeend', '
'); 92 | 93 | // Detect if swiping from screen edges to navigate back/forward 94 | var swipeNav = false; 95 | document.addEventListener('touchstart', function(e){ 96 | var touch = e.targetTouches[0]; 97 | var x = touch.clientX; 98 | if (x < 20 || x > window.innerWidth-20) swipeNav = true; 99 | }); 100 | document.addEventListener('touchend', function(e){ 101 | swipeNav = false; 102 | }); 103 | 104 | ruto.config({ 105 | before: function(path, name, matches){ 106 | var previousView = hw.previousView = hw.currentView; 107 | var currentView = hw.currentView = name; 108 | var hideAllViews = hw.hideAllViews; 109 | var view = $('view-' + currentView); 110 | hw.setTitle(view.querySelector('header h1').textContent); 111 | 112 | switch (currentView){ 113 | case 'home': 114 | if (!isWideScreen){ 115 | if (previousView == 'comments' && !swipeNav){ 116 | slide({ 117 | 'in': view, 118 | out: $('view-' + previousView), 119 | direction: 'ltr' 120 | }); 121 | } else if (previousView == 'about' && !swipeNav){ 122 | pop({ 123 | 'in': view, 124 | out: $('view-' + previousView), 125 | direction: 'down' 126 | }); 127 | } else { 128 | hideAllViews(); 129 | view.classList.remove('hidden'); 130 | } 131 | } else { 132 | hideAllViews(); 133 | $('overlay').classList.add('hide'); 134 | view.classList.remove('hidden'); 135 | var viewComments = $('view-comments'); 136 | viewComments.classList.remove('hidden'); 137 | viewComments.querySelector('section').innerHTML = '
No Story Selected.
'; 138 | viewComments.querySelector('header h1').innerHTML = ''; 139 | viewComments.querySelector('header a.header-back-button').style.display = 'none'; 140 | hw.comments.currentID = null; 141 | hw.pub('selectCurrentStory'); 142 | } 143 | break; 144 | case 'about': 145 | if (!isWideScreen){ 146 | if (previousView == 'home' && !swipeNav){ 147 | pop({ 148 | 'in': view, 149 | out: $('view-' + previousView), 150 | direction: 'up' 151 | }); 152 | } else { 153 | hideAllViews(); 154 | var $viewHome = $('view-home'); 155 | $viewHome.classList.remove('hidden'); 156 | view.classList.remove('hidden'); 157 | } 158 | } else { 159 | view.classList.remove('hidden'); 160 | $('view-home').classList.remove('hidden'); 161 | $('view-comments').classList.remove('hidden'); 162 | setTimeout(function(){ 163 | $('overlay').classList.remove('hide'); 164 | }, 1); 165 | } 166 | break; 167 | case 'comments': 168 | if (!isWideScreen){ 169 | if (previousView == 'home' && !swipeNav){ 170 | var id = matches[1]; 171 | if (id && hw.comments.currentID != id) view.querySelector('section').scrollTop = 0; 172 | slide({ 173 | 'in': view, 174 | out: $('view-' + previousView), 175 | direction: 'rtl' 176 | }); 177 | } else { 178 | hideAllViews(); 179 | view.classList.remove('hidden'); 180 | } 181 | } else { 182 | hideAllViews(); 183 | $('overlay').classList.add('hide'); 184 | view.classList.remove('hidden'); 185 | $('view-home').classList.remove('hidden'); 186 | hw.pub('selectCurrentStory', matches[1]); 187 | view.querySelector('header a.header-back-button').style.display = ''; 188 | } 189 | break; 190 | } 191 | } 192 | }); 193 | 194 | // Remember scroll tops of each views 195 | w.addEventListener('pagehide', function(){ 196 | var views = d.querySelectorAll('.view'), 197 | hackerScrollTops = {}; 198 | for (var i=0, l=views.length; iheader a.header-button[href]', { 215 | noScroll: true, 216 | onTap: function(e, target){ 217 | var hash = target.hash; 218 | // The close button in 'About' view 219 | if (isWideScreen && /about/i.test(ruto.current) && hash == '#/'){ 220 | ruto.back('/'); 221 | } else { 222 | location.hash = hash; 223 | } 224 | } 225 | }); 226 | 227 | tappable('#view-home-refresh', { 228 | noScroll: true, 229 | onTap: hw.news.reload 230 | }); 231 | 232 | var scrollingToTop = false; 233 | tappable('.view>header h1', { 234 | onTap: function(e, target){ 235 | var section = target.parentNode.nextElementSibling.firstElementChild; 236 | if (section.scrollTop == 0 || scrollingToTop) return; 237 | // Scroll the section to top 238 | // Reset the overflow because the momentum ignores scrollTop setting 239 | if (scrollingToTop) return; 240 | scrollingToTop = true; 241 | var originalOverflow = section.style.overflow; 242 | section.style.overflow = 'hidden'; 243 | setTimeout(function(){ 244 | section.style.overflow = originalOverflow; 245 | var raf; 246 | var tween = new TWEEN.Tween({scrollTop: section.scrollTop}) 247 | .to({scrollTop: 0}, 300) 248 | .easing(TWEEN.Easing.Cubic.InOut) 249 | .onUpdate(function(){ 250 | section.scrollTop = this.scrollTop; 251 | }) 252 | .onComplete(function(){ 253 | cancelAnimationFrame(raf); 254 | tween.stop(); // Removes the tween object 255 | scrollingToTop = false; 256 | delete tween; 257 | }) 258 | .start(); 259 | var step = function(){ 260 | TWEEN.update(); 261 | requestAnimationFrame(step); 262 | }; 263 | raf = requestAnimationFrame(step); 264 | }, 200); 265 | } 266 | }); 267 | 268 | // iPad-specific code for selected items in the list 269 | // When you tap on an item and drag, selected item will be deselected 270 | // When drag is done, previous selected item will be re-selected 271 | var listTappedDelay; 272 | tappable('#view-home .tableview-links li>a:first-child', { 273 | allowClick: !isWideScreen, 274 | activeClassDelay: 100, 275 | inactiveClassDelay: isWideScreen ? 100 : 1000, 276 | onStart: function(e, target){ 277 | if (isWideScreen){ 278 | var ul = target.parentNode; 279 | if (ul){ 280 | ul = ul.parentNode; 281 | listTappedDelay = setTimeout(function(){ 282 | if (ul) ul.classList.add('list-tapped'); 283 | }, 100); 284 | } 285 | } 286 | }, 287 | onMove: function(){ 288 | if (!isWideScreen) return; 289 | clearTimeout(listTappedDelay); 290 | }, 291 | onEnd: function(e, target){ 292 | if (!isWideScreen) return; 293 | clearTimeout(listTappedDelay); 294 | var ul = target.parentNode.parentNode; 295 | setTimeout(function(){ 296 | if (ul) ul.classList.remove('list-tapped'); 297 | }, 100); 298 | }, 299 | onTap: function(e, target){ 300 | if (target.classList.contains('more-link')){ 301 | hw.news.more(target); 302 | } else if (/^#\//.test(target.getAttribute('href'))){ // "local" links 303 | location.hash = target.hash; 304 | } else if (target.href && isWideScreen){ 305 | w.open(target.href); 306 | } 307 | } 308 | }); 309 | 310 | tappable('#view-about .grouped-tableview-links li>a:first-child', { 311 | allowClick: true, 312 | activeClassDelay: 100, 313 | inactiveClassDelay: 1000 314 | }); 315 | 316 | tappable('#view-home .tableview-links li>a.detail-disclosure-button', { 317 | noScroll: true, 318 | noScrollDelay: 100, 319 | onTap: function(e, target){ 320 | if (hw.currentView == 'comments') return; 321 | location.hash = target.hash; 322 | } 323 | }); 324 | 325 | tappable('button.comments-toggle', function(e, target){ 326 | hw.comments.toggle(target); 327 | }); 328 | 329 | tappable('#view-comments .load-error button', hw.comments.reload); 330 | 331 | hw.sub('selectCurrentStory', function(id){ 332 | if (!isWideScreen) return; 333 | if (!id) id = (location.hash.match(/item\/(\d+)/) || [,''])[1]; 334 | var homeView = $('view-home'); 335 | var selectedLinks = homeView.querySelectorAll('a[href].selected'); 336 | for (var i=0, l=selectedLinks.length; i= 640 ? 'wide' : 'narrow'; 72 | }; 73 | 74 | // Disable user scale of the viewport 75 | var vmeta = d.querySelector('meta[name=viewport]'); 76 | if (!vmeta){ 77 | vmeta = d.createElement('meta'); 78 | vmeta.name = 'viewport'; 79 | d.head.appendChild(vmeta); 80 | } 81 | vmeta.content = 'width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0'; 82 | 83 | // Apply iOS < 6 styles, only for iPhone/iPod 84 | var ua = navigator.userAgent, 85 | isIPhoneIPod = ua && /iPhone|iPod/.test(ua), 86 | isIOS5 = parseInt((ua.match(/ OS (\d+)_/i) || [,0])[1], 10) < 6; 87 | if (isIPhoneIPod && isIOS5) body.classList.add('ios5'); 88 | 89 | // Wide screen state 90 | var isWideScreen = getScreenState() == 'wide'; 91 | w.addEventListener('resize', function(){ 92 | var wide = getScreenState() == 'wide'; 93 | if (wide != isWideScreen){ 94 | isWideScreen = wide; 95 | location.reload(); 96 | } 97 | }); 98 | 99 | // Inject some elements for additional iOS decorations 100 | body.insertAdjacentHTML('beforeend', isWideScreen ? '
' : '
'); 101 | 102 | ruto.config({ 103 | before: function(path, name, matches){ 104 | var currentView = hw.currentView; 105 | var hideAllViews = hw.hideAllViews; 106 | var view = $('view-' + name); 107 | hw.setTitle(view.querySelector('header h1').textContent); 108 | 109 | switch (name){ 110 | case 'home': 111 | if (!isWideScreen){ 112 | if (!currentView){ 113 | hideAllViews(); 114 | view.classList.remove('hidden'); 115 | } else if (currentView == 'about'){ 116 | flip({ 117 | 'in': view, 118 | out: $('view-' + currentView), 119 | direction: 'anticlockwise' 120 | }); 121 | } else if (currentView != 'home'){ 122 | slide({ 123 | 'in': view, 124 | out: $('view-' + currentView), 125 | direction: 'ltr' 126 | }); 127 | } 128 | } else { 129 | hideAllViews(); 130 | $('overlay').classList.add('hide'); 131 | view.classList.remove('hidden'); 132 | var viewComments = $('view-comments'); 133 | viewComments.classList.remove('hidden'); 134 | viewComments.querySelector('section').innerHTML = '
No Story Selected.
'; 135 | viewComments.querySelector('header h1').innerHTML = ''; 136 | viewComments.querySelector('header a.header-back-button').style.display = 'none'; 137 | hw.comments.currentID = null; 138 | hw.pub('selectCurrentStory'); 139 | } 140 | hw.currentView = 'home'; 141 | break; 142 | case 'about': 143 | if (!isWideScreen){ 144 | if (!currentView){ 145 | hideAllViews(); 146 | view.classList.remove('hidden'); 147 | } else if (currentView != 'about'){ 148 | flip({ 149 | 'in': view, 150 | out: $('view-home'), 151 | direction: 'clockwise' 152 | }); 153 | } 154 | } else { 155 | view.classList.remove('hidden'); 156 | $('view-home').classList.remove('hidden'); 157 | $('view-comments').classList.remove('hidden'); 158 | setTimeout(function(){ 159 | $('overlay').classList.remove('hide'); 160 | }, 1); 161 | } 162 | hw.currentView = 'about'; 163 | break; 164 | case 'comments': 165 | if (!isWideScreen){ 166 | if (!currentView){ 167 | hideAllViews(); 168 | view.classList.remove('hidden'); 169 | } else if (currentView != 'comments'){ 170 | // Scroll to top first then slide, prevent Flash of Unscrolled View (FOUV) 171 | var id = matches[1]; 172 | if (id && hw.comments.currentID != id) view.querySelector('section').scrollTop = 0; 173 | slide({ 174 | 'in': view, 175 | out: $('view-' + currentView), 176 | direction: 'rtl' 177 | }); 178 | } 179 | } else { 180 | hideAllViews(); 181 | $('overlay').classList.add('hide'); 182 | view.classList.remove('hidden'); 183 | $('view-home').classList.remove('hidden'); 184 | hw.pub('selectCurrentStory', matches[1]); 185 | view.querySelector('header a.header-back-button').style.display = ''; 186 | } 187 | hw.currentView = 'comments'; 188 | break; 189 | } 190 | } 191 | }); 192 | 193 | // Remember scroll tops of each views 194 | w.addEventListener('pagehide', function(){ 195 | var views = d.querySelectorAll('.view'), 196 | hackerScrollTops = {}; 197 | for (var i=0, l=views.length; i.scroll'), 217 | wInnerHeight = null; 218 | Array.prototype.forEach.call($viewSections, function(view){ 219 | view.addEventListener('touchstart', function(){ 220 | if (w.innerHeight != wInnerHeight){ 221 | w.scrollTo(0, 0); 222 | if (isIOS5){ 223 | var div = d.createElement('div'); 224 | div.style.height = '600px'; 225 | body.appendChild(div); 226 | setTimeout(function(){ 227 | body.removeChild(div); 228 | }, 100); 229 | } 230 | wInnerHeight = w.innerHeight; 231 | } 232 | }, false); 233 | }); 234 | } 235 | 236 | tappable('.view>header a.header-button[href]', { 237 | noScroll: true, 238 | onTap: function(e, target){ 239 | var hash = target.hash; 240 | // The close button in 'About' view 241 | if (isWideScreen && /about/i.test(ruto.current) && hash == '#/'){ 242 | ruto.back('/'); 243 | } else { 244 | location.hash = hash; 245 | } 246 | } 247 | }); 248 | 249 | tappable('#view-home-refresh', { 250 | noScroll: true, 251 | onTap: hw.news.reload 252 | }); 253 | 254 | var scrollingToTop = false; 255 | tappable('.view>header h1', { 256 | onTap: function(e, target){ 257 | var section = target.parentNode.nextElementSibling.firstElementChild; 258 | if (section.scrollTop == 0 || scrollingToTop){ 259 | if (isIPhoneIPod){ 260 | // Show address bar 261 | var oriHeight = body.style.height; 262 | body.style.height = '100%'; 263 | setTimeout(function(){ 264 | body.style.height = oriHeight; 265 | }, 100); 266 | } 267 | } else { 268 | // Scroll the section to top 269 | // Reset the overflow because the momentum ignores scrollTop setting 270 | if (scrollingToTop) return; 271 | scrollingToTop = true; 272 | var originalOverflow = section.style.overflow; 273 | section.style.overflow = 'hidden'; 274 | setTimeout(function(){ 275 | section.style.overflow = originalOverflow; 276 | var raf; 277 | var tween = new TWEEN.Tween({scrollTop: section.scrollTop}) 278 | .to({scrollTop: 0}, 300) 279 | .easing(TWEEN.Easing.Cubic.InOut) 280 | .onUpdate(function(){ 281 | section.scrollTop = this.scrollTop; 282 | }) 283 | .onComplete(function(){ 284 | cancelAnimationFrame(raf); 285 | tween.stop(); // Removes the tween object 286 | scrollingToTop = false; 287 | delete tween; 288 | }) 289 | .start(); 290 | var step = function(){ 291 | TWEEN.update(); 292 | requestAnimationFrame(step); 293 | }; 294 | raf = requestAnimationFrame(step); 295 | }, 200); 296 | } 297 | } 298 | }); 299 | 300 | // iPad-specific code for selected items in the list 301 | // When you tap on an item and drag, selected item will be deselected 302 | // When drag is done, previous selected item will be re-selected 303 | var listTappedDelay; 304 | tappable('#view-home .tableview-links li>a:first-child', { 305 | allowClick: !isWideScreen, 306 | activeClassDelay: 100, 307 | inactiveClassDelay: isWideScreen ? 100 : 1000, 308 | onStart: function(e, target){ 309 | if (isWideScreen){ 310 | var ul = target.parentNode; 311 | if (ul){ 312 | ul = ul.parentNode; 313 | listTappedDelay = setTimeout(function(){ 314 | if (ul) ul.classList.add('list-tapped'); 315 | }, 100); 316 | } 317 | } 318 | }, 319 | onMove: function(){ 320 | if (!isWideScreen) return; 321 | clearTimeout(listTappedDelay); 322 | }, 323 | onEnd: function(e, target){ 324 | if (!isWideScreen) return; 325 | clearTimeout(listTappedDelay); 326 | var ul = target.parentNode.parentNode; 327 | setTimeout(function(){ 328 | if (ul) ul.classList.remove('list-tapped'); 329 | }, 100); 330 | }, 331 | onTap: function(e, target){ 332 | if (target.classList.contains('more-link')){ 333 | hw.news.more(target); 334 | } else if (/^#\//.test(target.getAttribute('href'))){ // "local" links 335 | location.hash = target.hash; 336 | } else if (target.href && isWideScreen){ 337 | w.open(target.href); 338 | } 339 | } 340 | }); 341 | 342 | tappable('#view-about .grouped-tableview-links li>a:first-child', { 343 | allowClick: true, 344 | activeClassDelay: 100, 345 | inactiveClassDelay: 1000 346 | }); 347 | 348 | tappable('#view-home .tableview-links li>a.detail-disclosure-button', { 349 | noScroll: true, 350 | noScrollDelay: 100, 351 | onTap: function(e, target){ 352 | location.hash = target.hash; 353 | } 354 | }); 355 | 356 | tappable('button.comments-toggle', function(e, target){ 357 | hw.comments.toggle(target); 358 | }); 359 | 360 | tappable('#view-comments .load-error button', hw.comments.reload); 361 | 362 | hw.sub('selectCurrentStory', function(id){ 363 | if (!isWideScreen) return; 364 | if (!id) id = (location.hash.match(/item\/(\d+)/) || [,''])[1]; 365 | var homeView = $('view-home'); 366 | var selectedLinks = homeView.querySelectorAll('a[href].selected'); 367 | for (var i=0, l=selectedLinks.length; i350ms, which is the sliding animation duration 412 | }); 413 | w.addEventListener('resize', adjustCommentsSection, false); 414 | w.addEventListener('orientationchange', adjustCommentsSection, false); 415 | 416 | // Some useful tips from http://24ways.org/2011/raising-the-bar-on-mobile 417 | var supportOrientation = typeof w.orientation != 'undefined', 418 | getScrollTop = function(){ 419 | return w.pageYOffset || d.compatMode === 'CSS1Compat' && d.documentElement.scrollTop || body.scrollTop || 0; 420 | }, 421 | scrollTop = function(){ 422 | if (!supportOrientation) return; 423 | body.style.height = screen.height + 'px'; 424 | setTimeout(function(){ 425 | w.scrollTo(0, 0); 426 | body.style.height = w.innerHeight + 'px'; 427 | }, 1); 428 | }; 429 | if (!isWideScreen){ 430 | if (d.readyState == 'complete'){ 431 | scrollTop(); 432 | } else { 433 | w.addEventListener('load', scrollTop, false); 434 | } 435 | if (supportOrientation) w.onorientationchange = scrollTop; 436 | } 437 | 438 | hw.news.options.disclosure = !isWideScreen; 439 | hw.init(); 440 | })(window); -------------------------------------------------------------------------------- /assets/js/hw-web.js: -------------------------------------------------------------------------------- 1 | (function(w){ 2 | if (!w.addEventListener) return; // If the browser doesn't even support this, just give up. 3 | 4 | var d = w.document, 5 | body = d.body, 6 | scrollTops = {}, 7 | scrollTimeout, 8 | getScrollTop = function(){ 9 | return w.pageYOffset || d.compatMode === 'CSS1Compat' && d.documentElement.scrollTop || 0; 10 | }, 11 | saveScrollTop = function(){ 12 | var hash = location.hash.slice(1); 13 | var top = scrollTops[hash] = getScrollTop(); 14 | var key = 'hacker-scrolltop-' + hash; 15 | amplify.store.sessionStorage(key, top); 16 | }; 17 | w.addEventListener('scroll', function(){ 18 | // debouncing scrolls 19 | clearTimeout(scrollTimeout); 20 | scrollTimeout = setTimeout(saveScrollTop, 500); 21 | }, false); 22 | ruto.config({ 23 | on: function(){ 24 | var hash = location.hash.slice(1); 25 | var key = 'hacker-scrolltop-' + hash; 26 | var top = amplify.store.sessionStorage(key); 27 | w.scrollTo(0, scrollTops[hash] || top || 0); 28 | top = scrollTops[hash] = getScrollTop(); 29 | amplify.store.sessionStorage(key, top); 30 | } 31 | }); 32 | 33 | // Adjust min-height on the views based on the viewport 34 | // While waiting for viewport units to be more widely supported by modern browsers 35 | var head = d.head || d.getElementsByTagName('head')[0]; 36 | var adjustViewsHeight = function(){ 37 | var vh = w.innerHeight; 38 | var style = $('view-height'); 39 | if (!style){ 40 | style = d.createElement('style'); 41 | style.id = 'view-height'; 42 | head.appendChild(style); 43 | } 44 | if (w.innerWidth >= 788) vh *= .9; 45 | style.textContent = '.view>.scroll{min-height: ' + vh + 'px}'; 46 | }; 47 | w.addEventListener('resize', adjustViewsHeight, false); 48 | w.addEventListener('orientationchange', adjustViewsHeight, false); 49 | adjustViewsHeight(); 50 | 51 | ibento('#view-home-refresh', 'click', hw.news.reload); 52 | 53 | ibento('#view-home .more-link', 'click', function(e, target){ 54 | hw.news.more(target); 55 | }); 56 | 57 | ibento('button.comments-toggle', 'click', function(e, target){ 58 | hw.comments.toggle(target); 59 | }); 60 | 61 | ibento('#view-comments .load-error button', 'click', hw.comments.reload); 62 | 63 | if (/Mobile;.*Firefox/.test(navigator.userAgent) && navigator.mozApps){ // Firefox Mobile 64 | var request = navigator.mozApps.getSelf(); // Check if installed on Firefox OS 65 | request.onsuccess = function(){ 66 | if (request.result){ 67 | // Bind all external links to window.open which invokes a system-provided "browser" window 68 | ibento('a[href]:not([href^="#"])', 'click', function(e, target){ 69 | e.preventDefault(); 70 | window.open(target.href, 'browser'); 71 | }); 72 | } 73 | } 74 | } 75 | 76 | window.onload = hw.init; 77 | })(window); 78 | -------------------------------------------------------------------------------- /assets/js/hw.js: -------------------------------------------------------------------------------- 1 | (function(w){ 2 | var d = w.document; 3 | 4 | var $ = w.$ = function(id){ 5 | return d.getElementById(id); 6 | }; 7 | 8 | var pubsubCache = {}, 9 | clone = function(obj){ 10 | var target = {}; 11 | for (var i in obj){ 12 | if (obj.hasOwnProperty(i)) target[i] = obj[i]; 13 | } 14 | return target; 15 | }; 16 | 17 | var hw = { 18 | // PubSub 19 | pub: function(topic, data){ 20 | var t = pubsubCache[topic]; 21 | if (!t) return; 22 | for (var i=0, l=t.length; i 3 && pathnameLen <= 15 && /^[^0-9][^.]+$/.test(pathname) ? ('/' + pathname) : ''; 72 | domained = domain + firstPath; 73 | domainsCache[url] = domained; 74 | return domained; 75 | }; 76 | 77 | var $homeScroll = d.querySelector('#view-home .scroll'), 78 | $homeScrollSection = $homeScroll.querySelector('section'), 79 | loadingNews = false; 80 | 81 | hw.news = { 82 | options: { 83 | disclosure: true 84 | }, 85 | markupStory: function(item){ 86 | if (/^item/i.test(item.url)){ 87 | item.url = '#/item/' + item.id; 88 | } else { 89 | item.external = true; 90 | item.domain = domainify(item.url); 91 | } 92 | if (!hw.news.options.disclosure){ 93 | if (item.id) item.url = '#/item/' + item.id; 94 | } else { 95 | if (item.type == 'link') item.detail_disclosure = true; 96 | if (/^#\//.test(item.url)){ 97 | item.detail_disclosure = false; 98 | item.disclosure = true; 99 | item.domain = null; 100 | } 101 | } 102 | item.i_point = item.points == 1 ? 'point' : 'points'; 103 | item.i_comment = item.comments_count == 1 ? 'comment' : 'comments'; 104 | return tmpl('post', item); 105 | }, 106 | markupStories: function(data, i){ 107 | var html = ''; 108 | if (!i) i = 1; 109 | var markupStory = hw.news.markupStory; 110 | data.forEach(function(item){ 111 | item.i = i++; 112 | html += markupStory(item); 113 | }); 114 | return html; 115 | }, 116 | // Re-markup the story item in the News list when 117 | // there's an update from specific API call of the item. 118 | // Make sure the title, points, comments count, etc matches. 119 | updateStory: function(story){ 120 | if (!story || !story.id) return; 121 | var id = story.id; 122 | var data = story.data; 123 | var post; 124 | var newsCache = 'hacker-news'; 125 | var news = amplify.store(newsCache); 126 | if (news){ 127 | for (var i=0, l=news.length; iMore…' : '') 179 | + ''; 180 | $homeScrollSection.innerHTML = html; 181 | hw.pub('onRenderNews'); 182 | }; 183 | if (cached){ 184 | var news = amplify.store('hacker-news'); 185 | var delay = opts.delay; 186 | if (delay){ 187 | loadingNews = true; 188 | $homeScrollSection.innerHTML = tmpl1.render({loading: true}); 189 | setTimeout(function(){ 190 | loadingNews = false; 191 | loadNews(news); 192 | }, delay); 193 | } else { 194 | loadNews(news); 195 | } 196 | } else { 197 | loadingNews = true; 198 | $homeScrollSection.innerHTML = tmpl1.render({loading: true}); 199 | var showError = function(){ 200 | $homeScrollSection.innerHTML = tmpl1.render({load_error: true}); 201 | hw.pub('logAPIError', 'news'); 202 | }; 203 | hnapi.news(function(data){ 204 | loadingNews = false; 205 | if (!data || data.error){ 206 | showError(); 207 | return; 208 | } 209 | amplify.store('hacker-news', data); 210 | amplify.store('hacker-news-cached', true, { 211 | expires: 1000*60*10 // 10 minutes 212 | }); 213 | amplify.store('hacker-news2', null); 214 | loadNews(data); 215 | // Preload news2 to prevent discrepancies between /news and /news2 results 216 | hnapi.news2(function(data){ 217 | if (!data || data.error) return; 218 | amplify.store('hacker-news2', data); 219 | $('hwlist').insertAdjacentHTML('beforeend', '
  • More…
  • '); 220 | }); 221 | }, function(e){ 222 | loadingNews = false; 223 | showError(); 224 | }); 225 | } 226 | }, 227 | reload: function(){ 228 | hw.news.render({ 229 | delay: 300 // Cheat a little to make user think that it's doing something 230 | }); 231 | }, 232 | more: function(target){ 233 | if (target.classList.contains('loading')) return; 234 | target.classList.add('loading'); 235 | var news2 = amplify.store('hacker-news2'); 236 | setTimeout(function(){ 237 | target.classList.remove('loading'); 238 | var targetParent = target.parentNode; 239 | if (!targetParent) return; 240 | if (targetParent.parentNode) targetParent.parentNode.removeChild(targetParent); 241 | if (!news2) return; 242 | var data = news2.slice(); 243 | var html = hw.news.markupStories(data, 31); 244 | $('hwlist').insertAdjacentHTML('beforeend', html); 245 | }, 400); 246 | } 247 | }; 248 | 249 | var $commentsView = $('view-comments'), 250 | $commentsHeading = $commentsView.querySelector('header h1'), 251 | $commentsSection = $commentsView.querySelector('section'); 252 | 253 | hw.comments = { 254 | currentID: null, 255 | render: function(id){ 256 | if (!id) return; 257 | var post = amplify.store.sessionStorage('hacker-item-' + id); 258 | if (hw.comments.currentID == id && post) return; 259 | hw.comments.currentID = id; 260 | 261 | var loadPost = function(_data, id){ 262 | var data = clone(_data), 263 | tmpl1 = tmpl('post-comments'); 264 | 265 | data.has_post = !!data.title; 266 | if (!data.has_post){ 267 | hw.setTitle(); 268 | $commentsHeading.innerText = ''; 269 | $commentsSection.innerHTML = tmpl1.render(data); 270 | hw.pub('adjustCommentsSection'); 271 | hw.pub('onRenderComments'); 272 | return; 273 | } 274 | 275 | var tmpl2 = tmpl('comments'); 276 | // If "local" link, link to Hacker News web site 277 | if (/^item/i.test(data.url)){ 278 | data.url = '//news.ycombinator.com/' + data.url; 279 | } else { 280 | data.domain = domainify(data.url); 281 | } 282 | data.has_comments = data.comments && !!data.comments.length; 283 | data.i_point = data.points == 1 ? 'point' : 'points'; 284 | data.i_comment = data.comments_count == 1 ? 'comment' : 'comments'; 285 | data.has_content = !!data.content; 286 | if (data.poll){ 287 | var total = 0; 288 | var max = 0; 289 | data.poll.forEach(function(p){ 290 | var points = p.points; 291 | if (points > max) max = points; 292 | total += points; 293 | p.i_point = points == 1 ? 'point' : 'points'; 294 | }); 295 | data.poll.forEach(function(p){ 296 | var points = p.points; 297 | p.percentage = (points/total*100).toFixed(1); 298 | p.width = (points/max*100).toFixed(1) + '%'; 299 | }); 300 | data.has_poll = data.has_content = true; 301 | } 302 | data.short_hn_url = 'news.ycombinator.com/item?id=' + id; 303 | data.hn_url = '//' + data.short_hn_url; 304 | hw.setTitle(data.title); 305 | $commentsHeading.innerText = data.title; 306 | 307 | var html = tmpl1.render(data, {comments_list: tmpl2}); 308 | var div = d.createElement('div'); 309 | div.innerHTML = html; 310 | 311 | // Make all links open in new tab/window 312 | // If it's a comment permalink, link to HN 313 | var links = div.querySelectorAll('a'); 314 | for (var i=0, l=links.length; i 20000){ 336 | var subUls = div.querySelectorAll('.comments>ul>li>ul'); 337 | var tmpl3 = tmpl('comments-toggle'); 338 | for (var j=0, l=subUls.length; j 0; i--){ 90 | var j = Math.floor(Math.random() * (i + 1)); 91 | var temp = array[i]; 92 | array[i] = array[j]; 93 | array[j] = temp; 94 | } 95 | }; 96 | shuffle(urls); 97 | urls.unshift('https://api.hackerwebapp.com/'); // The ultimate API 98 | 99 | var length = urls.length; 100 | var reqAgain = function(i, path, success, error){ 101 | var errorFunc = (i < length-1) ? function(){ 102 | reqAgain(i+1, path, success, error); 103 | } : error; 104 | req(urls[i] + path, success, errorFunc); 105 | }; 106 | var reqs = function(path, success, error){ 107 | req(urls[0] + path, success, function(){ 108 | reqAgain(0, path, success, error); 109 | }); 110 | }; 111 | 112 | var hnapi = { 113 | 114 | urls: urls, 115 | 116 | news: function(success, error){ 117 | reqs('news', success, error); 118 | }, 119 | 120 | news2: function(success, error){ 121 | reqs('news2', success, error); 122 | }, 123 | 124 | item: function(id, success, error){ 125 | reqs('item/' + id, success, error); 126 | }, 127 | 128 | comments: function(id, success, error){ 129 | reqs('comments/' + id, success, error); 130 | } 131 | 132 | }; 133 | 134 | w.hnapi = hnapi; 135 | 136 | })(window); 137 | -------------------------------------------------------------------------------- /assets/js/libs/hogan.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 Twitter, Inc. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | var Hogan = {}; 17 | 18 | (function (Hogan) { 19 | Hogan.Template = function (codeObj, text, compiler, options) { 20 | codeObj = codeObj || {}; 21 | this.r = codeObj.code || this.r; 22 | this.c = compiler; 23 | this.options = options || {}; 24 | this.text = text || ''; 25 | this.partials = codeObj.partials || {}; 26 | this.subs = codeObj.subs || {}; 27 | this.buf = ''; 28 | } 29 | 30 | Hogan.Template.prototype = { 31 | // render: replaced by generated code. 32 | r: function (context, partials, indent) { return ''; }, 33 | 34 | // variable escaping 35 | v: hoganEscape, 36 | 37 | // triple stache 38 | t: coerceToString, 39 | 40 | render: function render(context, partials, indent) { 41 | return this.ri([context], partials || {}, indent); 42 | }, 43 | 44 | // render internal -- a hook for overrides that catches partials too 45 | ri: function (context, partials, indent) { 46 | return this.r(context, partials, indent); 47 | }, 48 | 49 | // ensurePartial 50 | ep: function(symbol, partials) { 51 | var partial = this.partials[symbol]; 52 | 53 | // check to see that if we've instantiated this partial before 54 | var template = partials[partial.name]; 55 | if (partial.instance && partial.base == template) { 56 | return partial.instance; 57 | } 58 | 59 | if (typeof template == 'string') { 60 | if (!this.c) { 61 | throw new Error("No compiler available."); 62 | } 63 | template = this.c.compile(template, this.options); 64 | } 65 | 66 | if (!template) { 67 | return null; 68 | } 69 | 70 | // We use this to check whether the partials dictionary has changed 71 | this.partials[symbol].base = template; 72 | 73 | if (partial.subs) { 74 | // Make sure we consider parent template now 75 | if (!partials.stackText) partials.stackText = {}; 76 | for (key in partial.subs) { 77 | if (!partials.stackText[key]) { 78 | partials.stackText[key] = (this.activeSub !== undefined && partials.stackText[this.activeSub]) ? partials.stackText[this.activeSub] : this.text; 79 | } 80 | } 81 | template = createSpecializedPartial(template, partial.subs, partial.partials, 82 | this.stackSubs, this.stackPartials, partials.stackText); 83 | } 84 | this.partials[symbol].instance = template; 85 | 86 | return template; 87 | }, 88 | 89 | // tries to find a partial in the current scope and render it 90 | rp: function(symbol, context, partials, indent) { 91 | var partial = this.ep(symbol, partials); 92 | if (!partial) { 93 | return ''; 94 | } 95 | 96 | return partial.ri(context, partials, indent); 97 | }, 98 | 99 | // render a section 100 | rs: function(context, partials, section) { 101 | var tail = context[context.length - 1]; 102 | 103 | if (!isArray(tail)) { 104 | section(context, partials, this); 105 | return; 106 | } 107 | 108 | for (var i = 0; i < tail.length; i++) { 109 | context.push(tail[i]); 110 | section(context, partials, this); 111 | context.pop(); 112 | } 113 | }, 114 | 115 | // maybe start a section 116 | s: function(val, ctx, partials, inverted, start, end, tags) { 117 | var pass; 118 | 119 | if (isArray(val) && val.length === 0) { 120 | return false; 121 | } 122 | 123 | if (typeof val == 'function') { 124 | val = this.ms(val, ctx, partials, inverted, start, end, tags); 125 | } 126 | 127 | pass = !!val; 128 | 129 | if (!inverted && pass && ctx) { 130 | ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]); 131 | } 132 | 133 | return pass; 134 | }, 135 | 136 | // find values with dotted names 137 | d: function(key, ctx, partials, returnFound) { 138 | var found, 139 | names = key.split('.'), 140 | val = this.f(names[0], ctx, partials, returnFound), 141 | doModelGet = this.options.modelGet, 142 | cx = null; 143 | 144 | if (key === '.' && isArray(ctx[ctx.length - 2])) { 145 | val = ctx[ctx.length - 1]; 146 | } else { 147 | for (var i = 1; i < names.length; i++) { 148 | found = findInScope(names[i], val, doModelGet); 149 | if (found !== undefined) { 150 | cx = val; 151 | val = found; 152 | } else { 153 | val = ''; 154 | } 155 | } 156 | } 157 | 158 | if (returnFound && !val) { 159 | return false; 160 | } 161 | 162 | if (!returnFound && typeof val == 'function') { 163 | ctx.push(cx); 164 | val = this.mv(val, ctx, partials); 165 | ctx.pop(); 166 | } 167 | 168 | return val; 169 | }, 170 | 171 | // find values with normal names 172 | f: function(key, ctx, partials, returnFound) { 173 | var val = false, 174 | v = null, 175 | found = false, 176 | doModelGet = this.options.modelGet; 177 | 178 | for (var i = ctx.length - 1; i >= 0; i--) { 179 | v = ctx[i]; 180 | val = findInScope(key, v, doModelGet); 181 | if (val !== undefined) { 182 | found = true; 183 | break; 184 | } 185 | } 186 | 187 | if (!found) { 188 | return (returnFound) ? false : ""; 189 | } 190 | 191 | if (!returnFound && typeof val == 'function') { 192 | val = this.mv(val, ctx, partials); 193 | } 194 | 195 | return val; 196 | }, 197 | 198 | // higher order templates 199 | ls: function(func, cx, ctx, partials, text, tags) { 200 | var oldTags = this.options.delimiters; 201 | 202 | this.options.delimiters = tags; 203 | this.b(this.ct(coerceToString(func.call(cx, text, ctx)), cx, partials)); 204 | this.options.delimiters = oldTags; 205 | 206 | return false; 207 | }, 208 | 209 | // compile text 210 | ct: function(text, cx, partials) { 211 | if (this.options.disableLambda) { 212 | throw new Error('Lambda features disabled.'); 213 | } 214 | return this.c.compile(text, this.options).render(cx, partials); 215 | }, 216 | 217 | // template result buffering 218 | b: function(s) { this.buf += s; }, 219 | 220 | fl: function() { var r = this.buf; this.buf = ''; return r; }, 221 | 222 | // method replace section 223 | ms: function(func, ctx, partials, inverted, start, end, tags) { 224 | var textSource, 225 | cx = ctx[ctx.length - 1], 226 | result = func.call(cx); 227 | 228 | if (typeof result == 'function') { 229 | if (inverted) { 230 | return true; 231 | } else { 232 | textSource = (this.activeSub && this.subsText && this.subsText[this.activeSub]) ? this.subsText[this.activeSub] : this.text; 233 | return this.ls(result, cx, ctx, partials, textSource.substring(start, end), tags); 234 | } 235 | } 236 | 237 | return result; 238 | }, 239 | 240 | // method replace variable 241 | mv: function(func, ctx, partials) { 242 | var cx = ctx[ctx.length - 1]; 243 | var result = func.call(cx); 244 | 245 | if (typeof result == 'function') { 246 | return this.ct(coerceToString(result.call(cx)), cx, partials); 247 | } 248 | 249 | return result; 250 | }, 251 | 252 | sub: function(name, context, partials, indent) { 253 | var f = this.subs[name]; 254 | if (f) { 255 | this.activeSub = name; 256 | f(context, partials, this, indent); 257 | this.activeSub = false; 258 | } 259 | } 260 | 261 | }; 262 | 263 | //Find a key in an object 264 | function findInScope(key, scope, doModelGet) { 265 | var val; 266 | 267 | if (scope && typeof scope == 'object') { 268 | 269 | if (scope[key] !== undefined) { 270 | val = scope[key]; 271 | 272 | // try lookup with get for backbone or similar model data 273 | } else if (doModelGet && scope.get && typeof scope.get == 'function') { 274 | val = scope.get(key); 275 | } 276 | } 277 | 278 | return val; 279 | } 280 | 281 | function createSpecializedPartial(instance, subs, partials, stackSubs, stackPartials, stackText) { 282 | function PartialTemplate() {}; 283 | PartialTemplate.prototype = instance; 284 | function Substitutions() {}; 285 | Substitutions.prototype = instance.subs; 286 | var key; 287 | var partial = new PartialTemplate(); 288 | partial.subs = new Substitutions(); 289 | partial.subsText = {}; //hehe. substext. 290 | partial.buf = ''; 291 | 292 | stackSubs = stackSubs || {}; 293 | partial.stackSubs = stackSubs; 294 | partial.subsText = stackText; 295 | for (key in subs) { 296 | if (!stackSubs[key]) stackSubs[key] = subs[key]; 297 | } 298 | for (key in stackSubs) { 299 | partial.subs[key] = stackSubs[key]; 300 | } 301 | 302 | stackPartials = stackPartials || {}; 303 | partial.stackPartials = stackPartials; 304 | for (key in partials) { 305 | if (!stackPartials[key]) stackPartials[key] = partials[key]; 306 | } 307 | for (key in stackPartials) { 308 | partial.partials[key] = stackPartials[key]; 309 | } 310 | 311 | return partial; 312 | } 313 | 314 | var rAmp = /&/g, 315 | rLt = //g, 317 | rApos = /\'/g, 318 | rQuot = /\"/g, 319 | hChars = /[&<>\"\']/; 320 | 321 | function coerceToString(val) { 322 | return String((val === null || val === undefined) ? '' : val); 323 | } 324 | 325 | function hoganEscape(str) { 326 | str = coerceToString(str); 327 | return hChars.test(str) ? 328 | str 329 | .replace(rAmp, '&') 330 | .replace(rLt, '<') 331 | .replace(rGt, '>') 332 | .replace(rApos, ''') 333 | .replace(rQuot, '"') : 334 | str; 335 | } 336 | 337 | var isArray = Array.isArray || function(a) { 338 | return Object.prototype.toString.call(a) === '[object Array]'; 339 | }; 340 | 341 | })(typeof exports !== 'undefined' ? exports : Hogan); 342 | -------------------------------------------------------------------------------- /assets/js/libs/ibento.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ibento.js, super duper simple event delegation 3 | * 4 | * Copyright 2012, Lim Chee Aun (http://cheeaun.com/) 5 | * Licensed under the MIT license. 6 | * http://cheeaun.mit-license.org/ 7 | */ 8 | 9 | (function(w){ 10 | var d = w.document, 11 | matchesSelector = function(node, selector){ 12 | var root = d.documentElement, 13 | matches = root.matchesSelector || root.mozMatchesSelector || root.webkitMatchesSelector || root.oMatchesSelector || root.msMatchesSelector; 14 | return matches.call(node, selector); 15 | }, 16 | closest = function(node, selector){ 17 | var matches = false; 18 | do { 19 | matches = matchesSelector(node, selector); 20 | } while (!matches && (node = node.parentNode) && node.ownerDocument); 21 | return matches ? node : false; 22 | }; 23 | 24 | w.ibento = function(selector, event, fn){ 25 | var body = d.body; 26 | body.addEventListener(event, function(e){ 27 | var target = closest(e.target, selector); 28 | if (!target) return; 29 | fn.call(body, e, target); 30 | }); 31 | }; 32 | 33 | })(window); -------------------------------------------------------------------------------- /assets/js/libs/requestanimationframe.js: -------------------------------------------------------------------------------- 1 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 2 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 3 | 4 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 5 | 6 | // MIT license 7 | 8 | (function() { 9 | var lastTime = 0; 10 | var vendors = ['webkit', 'moz']; 11 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 12 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 13 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; 14 | } 15 | 16 | if (!window.requestAnimationFrame) 17 | window.requestAnimationFrame = function(callback, element) { 18 | var currTime = new Date().getTime(); 19 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 20 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 21 | timeToCall); 22 | lastTime = currTime + timeToCall; 23 | return id; 24 | }; 25 | 26 | if (!window.cancelAnimationFrame) 27 | window.cancelAnimationFrame = function(id) { 28 | clearTimeout(id); 29 | }; 30 | }()); 31 | -------------------------------------------------------------------------------- /assets/js/libs/ruto.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ruto.js, yet another simple hash router 3 | * 4 | * Copyright 2012, Lim Chee Aun (http://cheeaun.com/) 5 | * Licensed under the MIT license. 6 | * http://cheeaun.mit-license.org/ 7 | */ 8 | 9 | (function(w){ 10 | var routes = [], 11 | noop = function(){}, 12 | options = { 13 | defaultPath: '/', 14 | before: noop, 15 | on: noop, 16 | notfound: noop 17 | }; 18 | 19 | var ruto = { 20 | current: null, 21 | previous: null, 22 | config: function(opts){ 23 | for (var o in opts){ 24 | if (opts.hasOwnProperty(o)) options[o] = opts[o]; 25 | } 26 | return ruto; 27 | }, 28 | add: function(path, name, fn){ 29 | if (path && name){ 30 | if (typeof name == 'function'){ 31 | fn = name; 32 | name = null; 33 | } 34 | routes.push({ 35 | path: path, 36 | name: name, 37 | fn: fn || function(){} 38 | }); 39 | } 40 | return ruto; 41 | }, 42 | go: function(path){ 43 | location.hash = path; 44 | return ruto; 45 | }, 46 | back: function(path){ 47 | // Only 1-step back 48 | if (ruto.previous){ 49 | history.back(); 50 | ruto.previous = null; 51 | } else if (path){ // Fallback if can't go back 52 | location.hash = path; 53 | } 54 | return ruto; 55 | } 56 | }; 57 | 58 | var hashchange = function(){ 59 | var hash = location.hash.slice(1), 60 | found = false, 61 | current = ruto.current; 62 | 63 | if (!hash) hash = options.defaultPath; 64 | 65 | if (current && current != ruto.previous){ 66 | ruto.previous = current; 67 | } 68 | ruto.current = hash; 69 | 70 | for (var i=0, l=routes.length; i -1) return; 63 | el.className = clean(el.className + ' ' + className); 64 | }, 65 | removeClass = function(el, className){ 66 | if (!className) return; 67 | if (el.classList){ 68 | el.classList.remove(className); 69 | return; 70 | } 71 | el.className = el.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1'); 72 | }, 73 | matchesSelector = function(node, selector){ 74 | var root = d.documentElement, 75 | matches = root.matchesSelector || root.mozMatchesSelector || root.webkitMatchesSelector || root.oMatchesSelector || root.msMatchesSelector; 76 | return matches.call(node, selector); 77 | }, 78 | closest = function(node, selector){ 79 | var matches = false; 80 | do { 81 | matches = matchesSelector(node, selector); 82 | } while (!matches && (node = node.parentNode) && node.ownerDocument); 83 | return matches ? node : false; 84 | }; 85 | 86 | w.tappable = function(selector, opts){ 87 | if (typeof opts == 'function') opts = { onTap: opts }; 88 | var options = {}; 89 | for (var key in defaults) options[key] = opts[key] || defaults[key]; 90 | 91 | var el = options.containerElement || d.body, 92 | startTarget, 93 | prevTarget, 94 | startX, 95 | startY, 96 | elBound, 97 | cancel = false, 98 | moveOut = false, 99 | activeClass = options.activeClass, 100 | activeClassDelay = options.activeClassDelay, 101 | activeClassTimeout, 102 | inactiveClassDelay = options.inactiveClassDelay, 103 | inactiveClassTimeout, 104 | noScroll = options.noScroll, 105 | noScrollDelay = options.noScrollDelay, 106 | noScrollTimeout, 107 | boundMargin = options.boundMargin; 108 | 109 | var onStart = function(e){ 110 | var target = closest(getTarget(e), selector); 111 | if (!target) return; 112 | 113 | if (activeClassDelay){ 114 | clearTimeout(activeClassTimeout); 115 | activeClassTimeout = setTimeout(function(){ 116 | addClass(target, activeClass); 117 | }, activeClassDelay); 118 | } else { 119 | addClass(target, activeClass); 120 | } 121 | if (inactiveClassDelay && target == prevTarget) clearTimeout(inactiveClassTimeout); 122 | 123 | startX = e.clientX; 124 | startY = e.clientY; 125 | if (!startX || !startY){ 126 | var touch = e.targetTouches[0]; 127 | startX = touch.clientX; 128 | startY = touch.clientY; 129 | } 130 | startTarget = target; 131 | cancel = false; 132 | moveOut = false; 133 | elBound = noScroll ? target.getBoundingClientRect() : null; 134 | 135 | if (noScrollDelay){ 136 | clearTimeout(noScrollTimeout); 137 | noScroll = false; // set false first, then true after a delay 138 | noScrollTimeout = setTimeout(function(){ 139 | noScroll = true; 140 | }, noScrollDelay); 141 | } 142 | options.onStart.call(el, e, target); 143 | }; 144 | 145 | var onMove = function(e){ 146 | if (!startTarget) return; 147 | 148 | if (noScroll){ 149 | e.preventDefault(); 150 | } else { 151 | clearTimeout(activeClassTimeout); 152 | } 153 | 154 | var target = e.target, 155 | x = e.clientX, 156 | y = e.clientY; 157 | if (!target || !x || !y){ // The event might have a target but no clientX/Y 158 | var touch = e.changedTouches[0]; 159 | if (!x) x = touch.clientX; 160 | if (!y) y = touch.clientY; 161 | if (!target) target = getTargetByCoords(x, y); 162 | } 163 | 164 | if (noScroll){ 165 | if (x>elBound.left-boundMargin && xelBound.top-boundMargin && y 10){ 175 | cancel = true; 176 | removeClass(startTarget, activeClass); 177 | options.onCancel.call(target, e); 178 | } 179 | 180 | options.onMove.call(el, e, target); 181 | }; 182 | 183 | var onEnd = function(e){ 184 | if (!startTarget) return; 185 | 186 | clearTimeout(activeClassTimeout); 187 | if (inactiveClassDelay){ 188 | if (activeClassDelay && !cancel) addClass(startTarget, activeClass); 189 | var activeTarget = startTarget; 190 | inactiveClassTimeout = setTimeout(function(){ 191 | removeClass(activeTarget, activeClass); 192 | }, inactiveClassDelay); 193 | } else { 194 | removeClass(startTarget, activeClass); 195 | } 196 | 197 | options.onEnd.call(el, e, startTarget); 198 | 199 | var rightClick = e.which == 3 || e.button == 2; 200 | if (!cancel && !moveOut && !rightClick){ 201 | options.onTap.call(el, e, startTarget); 202 | } 203 | 204 | prevTarget = startTarget; 205 | startTarget = null; 206 | setTimeout(function(){ 207 | startX = startY = null; 208 | }, 400); 209 | }; 210 | 211 | var onCancel = function(e){ 212 | if (!startTarget) return; 213 | removeClass(startTarget, activeClass); 214 | startTarget = startX = startY = null; 215 | options.onCancel.call(el, e); 216 | }; 217 | 218 | var onClick = function(e){ 219 | var target = closest(e.target, selector); 220 | if (target){ 221 | e.preventDefault(); 222 | } else if (startX && startY && abs(e.clientX - startX) < 25 && abs(e.clientY - startY) < 25){ 223 | e.stopPropagation(); 224 | e.preventDefault(); 225 | } 226 | }; 227 | 228 | el.addEventListener(events.start, onStart, false); 229 | 230 | el.addEventListener(events.move, onMove, false); 231 | 232 | el.addEventListener(events.end, onEnd, false); 233 | 234 | el.addEventListener('touchcancel', onCancel, false); 235 | 236 | if (!options.allowClick) el.addEventListener('click', onClick, false); 237 | 238 | return { 239 | el : el, 240 | destroy : function () { 241 | el.removeEventListener(events.start, onStart, false); 242 | el.removeEventListener(events.move, onMove, false); 243 | el.removeEventListener(events.end, onEnd, false); 244 | el.removeEventListener('touchcancel', onCancel, false); 245 | if (!options.allowClick) el.removeEventListener('click', onClick, false); 246 | 247 | return this; 248 | } 249 | }; 250 | 251 | }; 252 | 253 | })); 254 | -------------------------------------------------------------------------------- /assets/js/libs/tween.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tween.js - Licensed under the MIT license 3 | * https://github.com/tweenjs/tween.js 4 | * ---------------------------------------------- 5 | * 6 | * See https://github.com/tweenjs/tween.js/graphs/contributors for the full list of contributors. 7 | * Thank you all, you're awesome! 8 | */ 9 | 10 | // Include a performance.now polyfill 11 | (function () { 12 | 13 | if ('performance' in window === false) { 14 | window.performance = {}; 15 | } 16 | 17 | // IE 8 18 | Date.now = (Date.now || function () { 19 | return new Date().getTime(); 20 | }); 21 | 22 | if ('now' in window.performance === false) { 23 | var offset = window.performance.timing && window.performance.timing.navigationStart ? window.performance.timing.navigationStart 24 | : Date.now(); 25 | 26 | window.performance.now = function () { 27 | return Date.now() - offset; 28 | }; 29 | } 30 | 31 | })(); 32 | 33 | var TWEEN = TWEEN || (function () { 34 | 35 | var _tweens = []; 36 | 37 | return { 38 | 39 | getAll: function () { 40 | 41 | return _tweens; 42 | 43 | }, 44 | 45 | removeAll: function () { 46 | 47 | _tweens = []; 48 | 49 | }, 50 | 51 | add: function (tween) { 52 | 53 | _tweens.push(tween); 54 | 55 | }, 56 | 57 | remove: function (tween) { 58 | 59 | var i = _tweens.indexOf(tween); 60 | 61 | if (i !== -1) { 62 | _tweens.splice(i, 1); 63 | } 64 | 65 | }, 66 | 67 | update: function (time) { 68 | 69 | if (_tweens.length === 0) { 70 | return false; 71 | } 72 | 73 | var i = 0; 74 | 75 | time = time !== undefined ? time : window.performance.now(); 76 | 77 | while (i < _tweens.length) { 78 | 79 | if (_tweens[i].update(time)) { 80 | i++; 81 | } else { 82 | _tweens.splice(i, 1); 83 | } 84 | 85 | } 86 | 87 | return true; 88 | 89 | } 90 | }; 91 | 92 | })(); 93 | 94 | TWEEN.Tween = function (object) { 95 | 96 | var _object = object; 97 | var _valuesStart = {}; 98 | var _valuesEnd = {}; 99 | var _valuesStartRepeat = {}; 100 | var _duration = 1000; 101 | var _repeat = 0; 102 | var _yoyo = false; 103 | var _isPlaying = false; 104 | var _reversed = false; 105 | var _delayTime = 0; 106 | var _startTime = null; 107 | var _easingFunction = TWEEN.Easing.Linear.None; 108 | var _interpolationFunction = TWEEN.Interpolation.Linear; 109 | var _chainedTweens = []; 110 | var _onStartCallback = null; 111 | var _onStartCallbackFired = false; 112 | var _onUpdateCallback = null; 113 | var _onCompleteCallback = null; 114 | var _onStopCallback = null; 115 | 116 | // Set all starting values present on the target object 117 | for (var field in object) { 118 | _valuesStart[field] = parseFloat(object[field], 10); 119 | } 120 | 121 | this.to = function (properties, duration) { 122 | 123 | if (duration !== undefined) { 124 | _duration = duration; 125 | } 126 | 127 | _valuesEnd = properties; 128 | 129 | return this; 130 | 131 | }; 132 | 133 | this.start = function (time) { 134 | 135 | TWEEN.add(this); 136 | 137 | _isPlaying = true; 138 | 139 | _onStartCallbackFired = false; 140 | 141 | _startTime = time !== undefined ? time : window.performance.now(); 142 | _startTime += _delayTime; 143 | 144 | for (var property in _valuesEnd) { 145 | 146 | // Check if an Array was provided as property value 147 | if (_valuesEnd[property] instanceof Array) { 148 | 149 | if (_valuesEnd[property].length === 0) { 150 | continue; 151 | } 152 | 153 | // Create a local copy of the Array with the start value at the front 154 | _valuesEnd[property] = [_object[property]].concat(_valuesEnd[property]); 155 | 156 | } 157 | 158 | // If `to()` specifies a property that doesn't exist in the source object, 159 | // we should not set that property in the object 160 | if (_valuesStart[property] === undefined) { 161 | continue; 162 | } 163 | 164 | _valuesStart[property] = _object[property]; 165 | 166 | if ((_valuesStart[property] instanceof Array) === false) { 167 | _valuesStart[property] *= 1.0; // Ensures we're using numbers, not strings 168 | } 169 | 170 | _valuesStartRepeat[property] = _valuesStart[property] || 0; 171 | 172 | } 173 | 174 | return this; 175 | 176 | }; 177 | 178 | this.stop = function () { 179 | 180 | if (!_isPlaying) { 181 | return this; 182 | } 183 | 184 | TWEEN.remove(this); 185 | _isPlaying = false; 186 | 187 | if (_onStopCallback !== null) { 188 | _onStopCallback.call(_object); 189 | } 190 | 191 | this.stopChainedTweens(); 192 | return this; 193 | 194 | }; 195 | 196 | this.stopChainedTweens = function () { 197 | 198 | for (var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++) { 199 | _chainedTweens[i].stop(); 200 | } 201 | 202 | }; 203 | 204 | this.delay = function (amount) { 205 | 206 | _delayTime = amount; 207 | return this; 208 | 209 | }; 210 | 211 | this.repeat = function (times) { 212 | 213 | _repeat = times; 214 | return this; 215 | 216 | }; 217 | 218 | this.yoyo = function (yoyo) { 219 | 220 | _yoyo = yoyo; 221 | return this; 222 | 223 | }; 224 | 225 | 226 | this.easing = function (easing) { 227 | 228 | _easingFunction = easing; 229 | return this; 230 | 231 | }; 232 | 233 | this.interpolation = function (interpolation) { 234 | 235 | _interpolationFunction = interpolation; 236 | return this; 237 | 238 | }; 239 | 240 | this.chain = function () { 241 | 242 | _chainedTweens = arguments; 243 | return this; 244 | 245 | }; 246 | 247 | this.onStart = function (callback) { 248 | 249 | _onStartCallback = callback; 250 | return this; 251 | 252 | }; 253 | 254 | this.onUpdate = function (callback) { 255 | 256 | _onUpdateCallback = callback; 257 | return this; 258 | 259 | }; 260 | 261 | this.onComplete = function (callback) { 262 | 263 | _onCompleteCallback = callback; 264 | return this; 265 | 266 | }; 267 | 268 | this.onStop = function (callback) { 269 | 270 | _onStopCallback = callback; 271 | return this; 272 | 273 | }; 274 | 275 | this.update = function (time) { 276 | 277 | var property; 278 | var elapsed; 279 | var value; 280 | 281 | if (time < _startTime) { 282 | return true; 283 | } 284 | 285 | if (_onStartCallbackFired === false) { 286 | 287 | if (_onStartCallback !== null) { 288 | _onStartCallback.call(_object); 289 | } 290 | 291 | _onStartCallbackFired = true; 292 | 293 | } 294 | 295 | elapsed = (time - _startTime) / _duration; 296 | elapsed = elapsed > 1 ? 1 : elapsed; 297 | 298 | value = _easingFunction(elapsed); 299 | 300 | for (property in _valuesEnd) { 301 | 302 | // Don't update properties that do not exist in the source object 303 | if (_valuesStart[property] === undefined) { 304 | continue; 305 | } 306 | 307 | var start = _valuesStart[property] || 0; 308 | var end = _valuesEnd[property]; 309 | 310 | if (end instanceof Array) { 311 | 312 | _object[property] = _interpolationFunction(end, value); 313 | 314 | } else { 315 | 316 | // Parses relative end values with start as base (e.g.: +10, -3) 317 | if (typeof (end) === 'string') { 318 | 319 | if (end.startsWith('+') || end.startsWith('-')) { 320 | end = start + parseFloat(end, 10); 321 | } else { 322 | end = parseFloat(end, 10); 323 | } 324 | } 325 | 326 | // Protect against non numeric properties. 327 | if (typeof (end) === 'number') { 328 | _object[property] = start + (end - start) * value; 329 | } 330 | 331 | } 332 | 333 | } 334 | 335 | if (_onUpdateCallback !== null) { 336 | _onUpdateCallback.call(_object, value); 337 | } 338 | 339 | if (elapsed === 1) { 340 | 341 | if (_repeat > 0) { 342 | 343 | if (isFinite(_repeat)) { 344 | _repeat--; 345 | } 346 | 347 | // Reassign starting values, restart by making startTime = now 348 | for (property in _valuesStartRepeat) { 349 | 350 | if (typeof (_valuesEnd[property]) === 'string') { 351 | _valuesStartRepeat[property] = _valuesStartRepeat[property] + parseFloat(_valuesEnd[property], 10); 352 | } 353 | 354 | if (_yoyo) { 355 | var tmp = _valuesStartRepeat[property]; 356 | 357 | _valuesStartRepeat[property] = _valuesEnd[property]; 358 | _valuesEnd[property] = tmp; 359 | } 360 | 361 | _valuesStart[property] = _valuesStartRepeat[property]; 362 | 363 | } 364 | 365 | if (_yoyo) { 366 | _reversed = !_reversed; 367 | } 368 | 369 | _startTime = time + _delayTime; 370 | 371 | return true; 372 | 373 | } else { 374 | 375 | if (_onCompleteCallback !== null) { 376 | _onCompleteCallback.call(_object); 377 | } 378 | 379 | for (var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++) { 380 | // Make the chained tweens start exactly at the time they should, 381 | // even if the `update()` method was called way past the duration of the tween 382 | _chainedTweens[i].start(_startTime + _duration); 383 | } 384 | 385 | return false; 386 | 387 | } 388 | 389 | } 390 | 391 | return true; 392 | 393 | }; 394 | 395 | }; 396 | 397 | 398 | TWEEN.Easing = { 399 | 400 | Linear: { 401 | 402 | None: function (k) { 403 | 404 | return k; 405 | 406 | } 407 | 408 | }, 409 | 410 | Quadratic: { 411 | 412 | In: function (k) { 413 | 414 | return k * k; 415 | 416 | }, 417 | 418 | Out: function (k) { 419 | 420 | return k * (2 - k); 421 | 422 | }, 423 | 424 | InOut: function (k) { 425 | 426 | if ((k *= 2) < 1) { 427 | return 0.5 * k * k; 428 | } 429 | 430 | return - 0.5 * (--k * (k - 2) - 1); 431 | 432 | } 433 | 434 | }, 435 | 436 | Cubic: { 437 | 438 | In: function (k) { 439 | 440 | return k * k * k; 441 | 442 | }, 443 | 444 | Out: function (k) { 445 | 446 | return --k * k * k + 1; 447 | 448 | }, 449 | 450 | InOut: function (k) { 451 | 452 | if ((k *= 2) < 1) { 453 | return 0.5 * k * k * k; 454 | } 455 | 456 | return 0.5 * ((k -= 2) * k * k + 2); 457 | 458 | } 459 | 460 | }, 461 | 462 | Quartic: { 463 | 464 | In: function (k) { 465 | 466 | return k * k * k * k; 467 | 468 | }, 469 | 470 | Out: function (k) { 471 | 472 | return 1 - (--k * k * k * k); 473 | 474 | }, 475 | 476 | InOut: function (k) { 477 | 478 | if ((k *= 2) < 1) { 479 | return 0.5 * k * k * k * k; 480 | } 481 | 482 | return - 0.5 * ((k -= 2) * k * k * k - 2); 483 | 484 | } 485 | 486 | }, 487 | 488 | Quintic: { 489 | 490 | In: function (k) { 491 | 492 | return k * k * k * k * k; 493 | 494 | }, 495 | 496 | Out: function (k) { 497 | 498 | return --k * k * k * k * k + 1; 499 | 500 | }, 501 | 502 | InOut: function (k) { 503 | 504 | if ((k *= 2) < 1) { 505 | return 0.5 * k * k * k * k * k; 506 | } 507 | 508 | return 0.5 * ((k -= 2) * k * k * k * k + 2); 509 | 510 | } 511 | 512 | }, 513 | 514 | Sinusoidal: { 515 | 516 | In: function (k) { 517 | 518 | return 1 - Math.cos(k * Math.PI / 2); 519 | 520 | }, 521 | 522 | Out: function (k) { 523 | 524 | return Math.sin(k * Math.PI / 2); 525 | 526 | }, 527 | 528 | InOut: function (k) { 529 | 530 | return 0.5 * (1 - Math.cos(Math.PI * k)); 531 | 532 | } 533 | 534 | }, 535 | 536 | Exponential: { 537 | 538 | In: function (k) { 539 | 540 | return k === 0 ? 0 : Math.pow(1024, k - 1); 541 | 542 | }, 543 | 544 | Out: function (k) { 545 | 546 | return k === 1 ? 1 : 1 - Math.pow(2, - 10 * k); 547 | 548 | }, 549 | 550 | InOut: function (k) { 551 | 552 | if (k === 0) { 553 | return 0; 554 | } 555 | 556 | if (k === 1) { 557 | return 1; 558 | } 559 | 560 | if ((k *= 2) < 1) { 561 | return 0.5 * Math.pow(1024, k - 1); 562 | } 563 | 564 | return 0.5 * (- Math.pow(2, - 10 * (k - 1)) + 2); 565 | 566 | } 567 | 568 | }, 569 | 570 | Circular: { 571 | 572 | In: function (k) { 573 | 574 | return 1 - Math.sqrt(1 - k * k); 575 | 576 | }, 577 | 578 | Out: function (k) { 579 | 580 | return Math.sqrt(1 - (--k * k)); 581 | 582 | }, 583 | 584 | InOut: function (k) { 585 | 586 | if ((k *= 2) < 1) { 587 | return - 0.5 * (Math.sqrt(1 - k * k) - 1); 588 | } 589 | 590 | return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); 591 | 592 | } 593 | 594 | }, 595 | 596 | Elastic: { 597 | 598 | In: function (k) { 599 | 600 | var s; 601 | var a = 0.1; 602 | var p = 0.4; 603 | 604 | if (k === 0) { 605 | return 0; 606 | } 607 | 608 | if (k === 1) { 609 | return 1; 610 | } 611 | 612 | if (!a || a < 1) { 613 | a = 1; 614 | s = p / 4; 615 | } else { 616 | s = p * Math.asin(1 / a) / (2 * Math.PI); 617 | } 618 | 619 | return - (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 620 | 621 | }, 622 | 623 | Out: function (k) { 624 | 625 | var s; 626 | var a = 0.1; 627 | var p = 0.4; 628 | 629 | if (k === 0) { 630 | return 0; 631 | } 632 | 633 | if (k === 1) { 634 | return 1; 635 | } 636 | 637 | if (!a || a < 1) { 638 | a = 1; 639 | s = p / 4; 640 | } else { 641 | s = p * Math.asin(1 / a) / (2 * Math.PI); 642 | } 643 | 644 | return (a * Math.pow(2, - 10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); 645 | 646 | }, 647 | 648 | InOut: function (k) { 649 | 650 | var s; 651 | var a = 0.1; 652 | var p = 0.4; 653 | 654 | if (k === 0) { 655 | return 0; 656 | } 657 | 658 | if (k === 1) { 659 | return 1; 660 | } 661 | 662 | if (!a || a < 1) { 663 | a = 1; 664 | s = p / 4; 665 | } else { 666 | s = p * Math.asin(1 / a) / (2 * Math.PI); 667 | } 668 | 669 | if ((k *= 2) < 1) { 670 | return - 0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); 671 | } 672 | 673 | return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; 674 | 675 | } 676 | 677 | }, 678 | 679 | Back: { 680 | 681 | In: function (k) { 682 | 683 | var s = 1.70158; 684 | 685 | return k * k * ((s + 1) * k - s); 686 | 687 | }, 688 | 689 | Out: function (k) { 690 | 691 | var s = 1.70158; 692 | 693 | return --k * k * ((s + 1) * k + s) + 1; 694 | 695 | }, 696 | 697 | InOut: function (k) { 698 | 699 | var s = 1.70158 * 1.525; 700 | 701 | if ((k *= 2) < 1) { 702 | return 0.5 * (k * k * ((s + 1) * k - s)); 703 | } 704 | 705 | return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); 706 | 707 | } 708 | 709 | }, 710 | 711 | Bounce: { 712 | 713 | In: function (k) { 714 | 715 | return 1 - TWEEN.Easing.Bounce.Out(1 - k); 716 | 717 | }, 718 | 719 | Out: function (k) { 720 | 721 | if (k < (1 / 2.75)) { 722 | return 7.5625 * k * k; 723 | } else if (k < (2 / 2.75)) { 724 | return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; 725 | } else if (k < (2.5 / 2.75)) { 726 | return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; 727 | } else { 728 | return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; 729 | } 730 | 731 | }, 732 | 733 | InOut: function (k) { 734 | 735 | if (k < 0.5) { 736 | return TWEEN.Easing.Bounce.In(k * 2) * 0.5; 737 | } 738 | 739 | return TWEEN.Easing.Bounce.Out(k * 2 - 1) * 0.5 + 0.5; 740 | 741 | } 742 | 743 | } 744 | 745 | }; 746 | 747 | TWEEN.Interpolation = { 748 | 749 | Linear: function (v, k) { 750 | 751 | var m = v.length - 1; 752 | var f = m * k; 753 | var i = Math.floor(f); 754 | var fn = TWEEN.Interpolation.Utils.Linear; 755 | 756 | if (k < 0) { 757 | return fn(v[0], v[1], f); 758 | } 759 | 760 | if (k > 1) { 761 | return fn(v[m], v[m - 1], m - f); 762 | } 763 | 764 | return fn(v[i], v[i + 1 > m ? m : i + 1], f - i); 765 | 766 | }, 767 | 768 | Bezier: function (v, k) { 769 | 770 | var b = 0; 771 | var n = v.length - 1; 772 | var pw = Math.pow; 773 | var bn = TWEEN.Interpolation.Utils.Bernstein; 774 | 775 | for (var i = 0; i <= n; i++) { 776 | b += pw(1 - k, n - i) * pw(k, i) * v[i] * bn(n, i); 777 | } 778 | 779 | return b; 780 | 781 | }, 782 | 783 | CatmullRom: function (v, k) { 784 | 785 | var m = v.length - 1; 786 | var f = m * k; 787 | var i = Math.floor(f); 788 | var fn = TWEEN.Interpolation.Utils.CatmullRom; 789 | 790 | if (v[0] === v[m]) { 791 | 792 | if (k < 0) { 793 | i = Math.floor(f = m * (1 + k)); 794 | } 795 | 796 | return fn(v[(i - 1 + m) % m], v[i], v[(i + 1) % m], v[(i + 2) % m], f - i); 797 | 798 | } else { 799 | 800 | if (k < 0) { 801 | return v[0] - (fn(v[0], v[0], v[1], v[1], -f) - v[0]); 802 | } 803 | 804 | if (k > 1) { 805 | return v[m] - (fn(v[m], v[m], v[m - 1], v[m - 1], f - m) - v[m]); 806 | } 807 | 808 | return fn(v[i ? i - 1 : 0], v[i], v[m < i + 1 ? m : i + 1], v[m < i + 2 ? m : i + 2], f - i); 809 | 810 | } 811 | 812 | }, 813 | 814 | Utils: { 815 | 816 | Linear: function (p0, p1, t) { 817 | 818 | return (p1 - p0) * t + p0; 819 | 820 | }, 821 | 822 | Bernstein: function (n, i) { 823 | 824 | var fc = TWEEN.Interpolation.Utils.Factorial; 825 | 826 | return fc(n) / fc(i) / fc(n - i); 827 | 828 | }, 829 | 830 | Factorial: (function () { 831 | 832 | var a = [1]; 833 | 834 | return function (n) { 835 | 836 | var s = 1; 837 | 838 | if (a[n]) { 839 | return a[n]; 840 | } 841 | 842 | for (var i = n; i > 1; i--) { 843 | s *= i; 844 | } 845 | 846 | a[n] = s; 847 | return s; 848 | 849 | }; 850 | 851 | })(), 852 | 853 | CatmullRom: function (p0, p1, p2, p3, t) { 854 | 855 | var v0 = (p2 - p0) * 0.5; 856 | var v1 = (p3 - p1) * 0.5; 857 | var t2 = t * t; 858 | var t3 = t * t2; 859 | 860 | return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (- 3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1; 861 | 862 | } 863 | 864 | } 865 | 866 | }; 867 | 868 | // UMD (Universal Module Definition) 869 | (function (root) { 870 | 871 | if (typeof define === 'function' && define.amd) { 872 | 873 | // AMD 874 | define([], function () { 875 | return TWEEN; 876 | }); 877 | 878 | } else if (typeof module !== 'undefined' && typeof exports === 'object') { 879 | 880 | // Node.js 881 | module.exports = TWEEN; 882 | 883 | } else if (root !== undefined) { 884 | 885 | // Global variable 886 | root.TWEEN = TWEEN; 887 | 888 | } 889 | 890 | })(this); 891 | -------------------------------------------------------------------------------- /assets/js/templates.js: -------------------------------------------------------------------------------- 1 | (function(t){ 2 | TEMPLATES = { 3 | 'comments-toggle': new t({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");return t.fl(); },partials: {}, subs: { }}), 4 | 'comments': new t({code: function (c,p,i) { var t=this;t.b(i=i||"");if(t.s(t.f("comments",c,p,1),c,p,0,13,418,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
  • ");if(!t.s(t.f("deleted",c,p,1),c,p,1,0,0,"")){t.b("

    ");t.b(t.t(t.f("content",c,p,0)));t.b("

      ");t.b(t.rp("");};t.b("");});c.pop();}return t.fl(); },partials: {"");if(t.s(t.f("has_post",c,p,1),c,p,0,39,573,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("

      ");t.b(t.v(t.f("title",c,p,0)));t.b("

      ");if(t.s(t.f("user",c,p,1),c,p,0,108,171,"{{ }}")){t.rs(c,p,function(c,p,t){if(t.s(t.f("domain",c,p,1),c,p,0,119,160,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("");t.b(t.v(t.f("domain",c,p,0)));t.b("");});c.pop();}});c.pop();}t.b("

      ");if(t.s(t.f("user",c,p,1),c,p,0,213,408,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("");t.b(t.v(t.f("points",c,p,0)));t.b(" ");t.b(t.v(t.f("i_point",c,p,0)));t.b(" by ");t.b(t.v(t.f("user",c,p,0)));t.b(" ");t.b(t.v(t.f("time_ago",c,p,0)));if(t.s(t.f("comments_count",c,p,1),c,p,0,340,382,"{{ }}")){t.rs(c,p,function(c,p,t){t.b(" · ");t.b(t.v(t.f("comments_count",c,p,0)));t.b(" ");t.b(t.v(t.f("i_comment",c,p,0)));});c.pop();}t.b("");});c.pop();}if(!t.s(t.f("user",c,p,1),c,p,1,0,0,"")){t.b("");t.b(t.v(t.f("time_ago",c,p,0)));t.b("");};t.b("");t.b(t.v(t.f("short_hn_url",c,p,0)));t.b("

      ");});c.pop();}if(t.s(t.f("has_content",c,p,1),c,p,0,602,925,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      ");t.b(t.t(t.f("content",c,p,0)));if(t.s(t.f("has_poll",c,p,1),c,p,0,663,902,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
        ");if(t.s(t.f("poll",c,p,1),c,p,0,689,888,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      • ");t.b(t.v(t.f("item",c,p,0)));t.b(" ");t.b(t.v(t.f("points",c,p,0)));t.b(" ");t.b(t.v(t.f("i_point",c,p,0)));t.b("
      • ");});c.pop();}t.b("
      ");});c.pop();}t.b("
      ");});c.pop();}t.b("
      ");if(t.s(t.f("loading",c,p,1),c,p,0,985,1055,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      Loading…
      ");});c.pop();}if(t.s(t.f("load_error",c,p,1),c,p,0,1082,1165,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      Couldn't load comments.
      ");});c.pop();}if(!t.s(t.f("loading",c,p,1),c,p,1,0,0,"")){if(!t.s(t.f("load_error",c,p,1),c,p,1,0,0,"")){if(t.s(t.f("has_comments",c,p,1),c,p,0,1224,1251,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
        ");t.b(t.rp("");});c.pop();}if(!t.s(t.f("has_comments",c,p,1),c,p,1,0,0,"")){t.b("

        No comments.

        ");};};};t.b("
      ");return t.fl(); },partials: {"
      ");t.b(t.v(t.f("i",c,p,0)));t.b("
      ");t.b(t.v(t.f("title",c,p,0)));t.b("");if(t.s(t.f("user",c,p,1),c,p,0,301,585,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      ");if(t.s(t.f("domain",c,p,1),c,p,0,334,373,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("");});c.pop();}t.b("");t.b(t.v(t.f("points",c,p,0)));t.b(" ");t.b(t.v(t.f("i_point",c,p,0)));t.b(" by ");t.b(t.v(t.f("user",c,p,0)));t.b(" ");t.b(t.v(t.f("time_ago",c,p,0)));if(t.s(t.f("comments_count",c,p,1),c,p,0,511,553,"{{ }}")){t.rs(c,p,function(c,p,t){t.b(" · ");t.b(t.v(t.f("comments_count",c,p,0)));t.b(" ");t.b(t.v(t.f("i_comment",c,p,0)));});c.pop();}t.b("
      ");});c.pop();}if(!t.s(t.f("user",c,p,1),c,p,1,0,0,"")){t.b("
      ");if(t.s(t.f("domain",c,p,1),c,p,0,636,681,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("");t.b(t.v(t.f("domain",c,p,0)));t.b("
      ");});c.pop();}t.b("");t.b(t.v(t.f("time_ago",c,p,0)));t.b("
      ");};t.b("
      ");if(t.s(t.f("detail_disclosure",c,p,1),c,p,0,785,859,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("");});c.pop();}t.b("");return t.fl(); },partials: {}, subs: { }}), 7 | 'stories-load': new t({code: function (c,p,i) { var t=this;t.b(i=i||"");if(t.s(t.f("loading",c,p,1),c,p,0,12,82,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      Loading…
      ");});c.pop();}if(t.s(t.f("load_error",c,p,1),c,p,0,109,161,"{{ }}")){t.rs(c,p,function(c,p,t){t.b("
      Couldn't load stories.
      ");});c.pop();}return t.fl(); },partials: {}, subs: { }}) 8 | } 9 | })(Hogan.Template); -------------------------------------------------------------------------------- /assets/templates/comments-toggle.mustache: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/templates/comments.mustache: -------------------------------------------------------------------------------- 1 | {{#comments}} 2 |
    • 3 | 8 | {{^deleted}} 9 |

      {{{content}}} 10 |

        11 | {{>comments_list}} 12 |
      13 | {{/deleted}} 14 |
    • 15 | {{/comments}} 16 | -------------------------------------------------------------------------------- /assets/templates/post-comments.mustache: -------------------------------------------------------------------------------- 1 |
      2 | {{#has_post}} 3 |
      4 | 5 |

      {{title}}

      6 | {{#user}} 7 | {{#domain}}{{domain}}{{/domain}} 8 | {{/user}} 9 |
      10 | 20 |
      21 | {{/has_post}} 22 | {{#has_content}} 23 |
      24 | {{{content}}} 25 | {{#has_poll}} 26 |
        27 | {{#poll}} 28 |
      • 29 | {{item}} {{points}} {{i_point}} 30 |
        31 |
      • 32 | {{/poll}} 33 |
      34 | {{/has_poll}} 35 |
      36 | {{/has_content}} 37 |
      38 |
      39 | {{#loading}} 40 |
      Loading…
      41 | {{/loading}} 42 | {{#load_error}} 43 |
      Couldn't load comments.
      44 | {{/load_error}} 45 | {{^loading}} 46 | {{^load_error}} 47 | {{#has_comments}} 48 |
        49 | {{>comments_list}} 50 |
      51 | {{/has_comments}} 52 | {{^has_comments}} 53 |

      No comments.

      54 | {{/has_comments}} 55 | {{/load_error}} 56 | {{/loading}} 57 |
      58 | -------------------------------------------------------------------------------- /assets/templates/post.mustache: -------------------------------------------------------------------------------- 1 |
    • 2 | 3 |
      {{i}}
      4 |
      5 | {{title}} 6 | {{#user}} 7 | 12 | {{/user}} 13 | {{^user}} 14 | 18 | {{/user}} 19 |
      20 |
      21 | {{#detail_disclosure}} 22 | 23 | {{/detail_disclosure}} 24 |
    • 25 | -------------------------------------------------------------------------------- /assets/templates/stories-load.mustache: -------------------------------------------------------------------------------- 1 | {{#loading}} 2 |
      Loading…
      3 | {{/loading}} 4 | {{#load_error}} 5 |
      Couldn't load stories.
      6 | {{/load_error}} -------------------------------------------------------------------------------- /chrome-web-app/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/chrome-web-app/icon-128.png -------------------------------------------------------------------------------- /chrome-web-app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackerWeb", 3 | "description": "A simply readable Hacker News web app", 4 | "version": "0.3", 5 | "manifest_version": 2, 6 | "app": { 7 | "launch": { 8 | "web_url": "https://cheeaun.github.io/hackerweb/" 9 | } 10 | }, 11 | "icons": { 12 | "128": "icon-128.png" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /icons/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/favicon-128.png -------------------------------------------------------------------------------- /icons/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/favicon-196.png -------------------------------------------------------------------------------- /icons/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/favicon-32.png -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/favicon.ico -------------------------------------------------------------------------------- /icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/favicon.png -------------------------------------------------------------------------------- /icons/fxos-icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/fxos-icon-128.png -------------------------------------------------------------------------------- /icons/fxos-icon-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/fxos-icon-30.png -------------------------------------------------------------------------------- /icons/fxos-icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/fxos-icon-60.png -------------------------------------------------------------------------------- /icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/icon-128.png -------------------------------------------------------------------------------- /icons/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/icon-180.png -------------------------------------------------------------------------------- /icons/icon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/icon-196.png -------------------------------------------------------------------------------- /icons/icon-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/icon-30.png -------------------------------------------------------------------------------- /icons/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/icon-60.png -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /icons/touch-icon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-114.png -------------------------------------------------------------------------------- /icons/touch-icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-120.png -------------------------------------------------------------------------------- /icons/touch-icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-144.png -------------------------------------------------------------------------------- /icons/touch-icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-152.png -------------------------------------------------------------------------------- /icons/touch-icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-72.png -------------------------------------------------------------------------------- /icons/touch-icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/icons/touch-icon-76.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HackerWeb 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 41 | 70 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /js/hnapi-worker.js: -------------------------------------------------------------------------------- 1 | var requests = {}; 2 | 3 | addEventListener('message', function(e){ 4 | var data = e.data, 5 | url = data.url, 6 | timeout = data.timeout || 20000; 7 | var r = requests[url] || new XMLHttpRequest(); 8 | if (r._timeout) clearTimeout(r._timeout); 9 | r._timeout = setTimeout(function(){ 10 | r.abort(); 11 | }, timeout); 12 | r.onload = function(){ 13 | clearTimeout(this._timeout); 14 | delete requests[url]; 15 | var response; 16 | try { 17 | response = JSON.parse(this.responseText); 18 | } catch (e){} 19 | if (this.status == 200 && response && !response.error){ 20 | postMessage({ 21 | url: url, 22 | response: response 23 | }); 24 | } else { 25 | postMessage({ 26 | url: url, 27 | error: true 28 | }); 29 | } 30 | }; 31 | r.onerror = r.onabort = r.ontimeout = function(e){ 32 | clearTimeout(this._timeout); 33 | delete requests[url]; 34 | postMessage({ 35 | url: url, 36 | error: JSON.parse(JSON.stringify(e)) 37 | }); 38 | } 39 | if (r.readyState <= 1){ 40 | r.open('GET', url, true); 41 | r.send(); 42 | } 43 | requests[url] = r; 44 | }); 45 | -------------------------------------------------------------------------------- /js/hw-web.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(){}function n(){var e=location.hash.slice(1),t=!1,n=p.current,e=e||f.defaultPath;n&&n!=p.previous&&(p.previous=n),p.current=e;for(var o=0,r=u.length;o/g,r=/\'/g,i=/\"/g,s=/[&<>\"\']/;function a(e){return String(null==e?"":e)}var f=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}}("undefined"!=typeof exports?exports:Hogan),function(a){var l=!!a.XDomainRequest,c="withCredentials"in new XMLHttpRequest||l,u=!1,f={};try{(u=new Worker("js/hnapi-worker.js")).addEventListener("message", 16 | function(e){var t,n,o,r=e.data,i=r.url||"";f[i]&&(n=(t=f[i]).error,o=t.success,delete f[i],r.error?n(r.error):o(r.response))},!1)}catch(e){}function i(t,e,n){var o,r,i,s;e=e||function(){},n=n||function(){},c?u?(f[t]={success:e,error:n},u.postMessage({url:t,timeout:2e4})):((o=f[t]||new(l?XDomainRequest:XMLHttpRequest))._timeout&&clearTimeout(o._timeout),o._timeout=setTimeout(function(){o.abort()},2e4),o.onload=function(){clearTimeout(this._timeout),delete f[t];try{e(JSON.parse(this.responseText)) 17 | }catch(e){n(e)}},o.onerror=o.onabort=o.ontimeout=function(e){clearTimeout(this._timeout),delete f[t],n(e)},(o.readyState<=1||l)&&(o.open("GET",t,!0),o.send()),f[t]=o):(i=(r=a.document).createElement("script"),s="callback"+ +new Date,a[s]=e,i.onerror=n,i.src=t+"?callback="+s,r.body.appendChild(i))}var s=["https://node-hnapi-eu.herokuapp.com/","https://node-hnapi.azurewebsites.net/","https://node-hnapi-eus.azurewebsites.net/"];!function(e){for(var t=e.length-1;0'),o.b(o.v(o.f("comments_count",e,t,0)) 20 | ),o.b(" "),o.b(o.v(o.f("i_reply",e,t,0))),o.b(""),o.fl()},partials:{},subs:{}}),comments:new e({code:function(e,t,n){var o=this;return o.b(n=n||""),o.s(o.f("comments",e,t,1),e,t,0,13,418,"{{ }}")&&(o.rs(e,t,function(e,t,n){n.b('
    • "),n.s(n.f("deleted",e,t,1),e,t,1,0,0,"")||(n.b("

      "),n.b(n.t(n.f("content",e,t,0))),n.b("

      ";l.innerHTML=n,_.pub("onRenderNews")},t?(r=amplify.store("hacker-news"),(i=e.delay)?(c=!0,l.innerHTML=n.render({loading:!0}),setTimeout(function(){c=!1,o(r)},i)):o(r)):(c=!0,l.innerHTML=n.render({loading:!0}),s=function(){l.innerHTML=n.render({load_error:!0} 39 | ),_.pub("logAPIError","news")},hnapi.news(function(e){c=!1,e&&!e.error?(amplify.store("hacker-news",e),amplify.store("hacker-news-cached",!0,{expires:6e5}),amplify.store("hacker-news2",null),o(e),hnapi.news2(function(e){e&&!e.error&&(amplify.store("hacker-news2",e),f("hwlist").insertAdjacentHTML("beforeend",'
    • More…
    • '))})):s()},function(e){c=!1,s()})))},reload:function(){_.news.render({delay:300})},more:function(o){var r 40 | ;o.classList.contains("loading")||(o.classList.add("loading"),r=amplify.store("hacker-news2"),setTimeout(function(){o.classList.remove("loading");var e,t,n=o.parentNode;n&&(n.parentNode&&n.parentNode.removeChild(n),r&&(e=r.slice(),t=_.news.markupStories(e,31),f("hwlist").insertAdjacentHTML("beforeend",t)))},400))}};var t=f("view-comments"),x=t.querySelector("header h1"),T=t.querySelector("section");_.comments={currentID:null,render:function(t){if(t){var e=amplify.store.sessionStorage( 41 | "hacker-item-"+t);if(_.comments.currentID!=t||!e){_.comments.currentID=t;function n(e,t){var n=function(e){var t={};for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}(e),o=k("post-comments");if(n.has_post=!!n.title,!n.has_post)return _.setTitle(),x.innerText="",T.innerHTML=o.render(n),_.pub("adjustCommentsSection"),void _.pub("onRenderComments");var r,i,s=k("comments");/^item/i.test(n.url)?n.url="//news.ycombinator.com/"+n.url:n.domain=S(n.url), 42 | n.has_comments=n.comments&&!!n.comments.length,n.i_point=1==n.points?"point":"points",n.i_comment=1==n.comments_count?"comment":"comments",n.has_content=!!n.content,n.poll&&(i=r=0,n.poll.forEach(function(e){var t=e.points;iul>li>ul"),b=k("comments-toggle"),v=0,f=h.length;v.scroll{min-height: "+e+"px}"},o.addEventListener("resize",a,!1),o.addEventListener("orientationchange",a,!1),a(),ibento("#view-home-refresh","click", 51 | hw.news.reload),ibento("#view-home .more-link","click",function(e,t){hw.news.more(t)}),ibento("button.comments-toggle","click",function(e,t){hw.comments.toggle(t)}),ibento("#view-comments .load-error button","click",hw.comments.reload),/Mobile;.*Firefox/.test(navigator.userAgent)&&navigator.mozApps&&((l=navigator.mozApps.getSelf()).onsuccess=function(){l.result&&ibento('a[href]:not([href^="#"])',"click",function(e,t){e.preventDefault(),window.open(t.href,"browser")})}),window.onload=hw.init)}( 52 | window); 53 | //# sourceMappingURL=hw-web.min.js.map -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackerWeb", 3 | "short_name": "HackerWeb", 4 | "description": "A simply readable Hacker News app.", 5 | "icons": [ 6 | { 7 | "src": "icons/icon.svg", 8 | "sizes": "144x144", 9 | "type": "image/svg+xml" 10 | } 11 | ], 12 | "start_url": ".", 13 | "display": "standalone", 14 | "theme_color": "#fff" 15 | } 16 | -------------------------------------------------------------------------------- /manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackerWeb", 3 | "description": "A simply readable Hacker News app", 4 | "version": "0.3", 5 | "launch_path": "/", 6 | "icons": { 7 | "30": "/icons/icon-30.png", 8 | "60": "/icons/icon-60.png", 9 | "128": "/icons/icon-128.png" 10 | }, 11 | "developer": { 12 | "name": "Lim Chee Aun", 13 | "url": "http://cheeaun.com/" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HackerWeb Options 7 | 8 | 9 | 43 | 44 | 45 |
      46 |

      HackerWeb Options

      47 |
      48 |
        49 |
      • 50 | UI Theme (experimental) 51 | 52 | 53 | 54 | 55 |
      • 56 |
      57 |
      58 | 59 |

      Note: Reload HackerWeb after changing these options.

      60 |
      61 | 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackerweb", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "grunt": "^1.3.0", 6 | "grunt-concurrent": "~3.0.0", 7 | "grunt-contrib-connect": "~3.0.0", 8 | "grunt-contrib-cssmin": "~3.0.0", 9 | "grunt-contrib-uglify": "~5.0.0", 10 | "grunt-contrib-watch": "^1.1.0", 11 | "hogan.js": "git://github.com/twitter/hogan.js.git", 12 | "mime": "~2.5.0" 13 | }, 14 | "scripts": { 15 | "start": "grunt server", 16 | "deploy": "git checkout gh-pages && git merge master && git push origin gh-pages && git checkout master" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /promo-images/promo-440-280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/promo-images/promo-440-280.png -------------------------------------------------------------------------------- /screenshots/screenshot-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot-chrome.png -------------------------------------------------------------------------------- /screenshots/screenshot-fxos-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot-fxos-1.png -------------------------------------------------------------------------------- /screenshots/screenshot-fxos-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot-fxos-2.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot1@2x.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/screenshots/screenshot2@2x.png -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export HACKERWEB_PORT=3000 4 | grunt server 5 | -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event){ 2 | console.log('Install'); 3 | }); 4 | 5 | self.addEventListener('activate', function(event){ 6 | console.log('Activate'); 7 | }); 8 | 9 | var cacheName = 'hackerweb-v1'; 10 | var successResponses = /^0|([123]\d\d)|(40[14567])|410$/; 11 | 12 | function fetchAndCache(request){ 13 | console.log('fetchAndCache', request.url); 14 | return fetch(request.clone()).then(function(response){ 15 | console.log(request.method, request.url, response.status, response.type); 16 | if (request.method == 'GET' && response && successResponses.test(response.status) && response.type == 'basic'){ 17 | console.log('Cache', request.url); 18 | caches.open(cacheName).then(function(cache){ 19 | cache.put(request, response); 20 | }); 21 | } 22 | return response.clone(); 23 | }); 24 | }; 25 | 26 | function cacheOnly(request){ 27 | console.log('cacheOnly', request.url); 28 | return caches.open(cacheName).then(function(cache){ 29 | return cache.match(request); 30 | }); 31 | }; 32 | 33 | // Fastest strategy from https://github.com/GoogleChrome/sw-toolbox 34 | self.addEventListener('fetch', function(event){ 35 | var request = event.request; 36 | var url = request.url; 37 | console.log('Fetch', url); 38 | event.respondWith(new Promise(function(resolve, reject){ 39 | var rejected = false; 40 | var reasons = []; 41 | 42 | var maybeReject = function(reason){ 43 | reasons.push(reason.toString()); 44 | if (rejected){ 45 | reject(new Error('Both cache and network failed: "' + reasons.join('", "') + '"')); 46 | } else { 47 | rejected = true; 48 | } 49 | }; 50 | 51 | var maybeResolve = function(result){ 52 | if (result instanceof Response){ 53 | resolve(result); 54 | } else { 55 | maybeReject('No result returned'); 56 | } 57 | }; 58 | 59 | fetchAndCache(request.clone()).then(maybeResolve, maybeReject); 60 | cacheOnly(request).then(maybeResolve, maybeReject); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /sources/hackerweb-logo.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/hackerweb-logo.xar -------------------------------------------------------------------------------- /sources/ios-cubic-bezier.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/ios-cubic-bezier.xar -------------------------------------------------------------------------------- /sources/ios-elements.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/ios-elements.xar -------------------------------------------------------------------------------- /sources/ios7-elements.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/ios7-elements.xar -------------------------------------------------------------------------------- /sources/landing.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/landing.xar -------------------------------------------------------------------------------- /sources/loader.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/loader.xar -------------------------------------------------------------------------------- /sources/misc.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/misc.xar -------------------------------------------------------------------------------- /sources/promo-image.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/promo-image.afdesign -------------------------------------------------------------------------------- /sources/ycombinator-logo.xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/sources/ycombinator-logo.xar -------------------------------------------------------------------------------- /startups/startup-320-460.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/startups/startup-320-460.png -------------------------------------------------------------------------------- /startups/startup-640-920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/hackerweb/84044b4b4301d4a0261c726fbf351bf27323b5e9/startups/startup-640-920.png -------------------------------------------------------------------------------- /tasks/embedImage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var mime = require('mime'); 5 | 6 | module.exports = function(grunt){ 7 | 8 | grunt.registerMultiTask('embedImage', 'Embed images into CSS files', function(){ 9 | 10 | var cache = {}; 11 | 12 | this.filesSrc.forEach(function(f){ 13 | var file = grunt.file.read(f); 14 | var filePath = path.dirname(f); 15 | var matched = false; 16 | 17 | var newFile = file.replace(/(url\()[^()]+(\)[^\/]+\/\*\s*embedImage:url\()([^()]+)/igm, function(match, s1, s2, image){ 18 | matched = true; 19 | var data = cache[image]; 20 | if (!data){ 21 | var p = path.join(filePath, image); 22 | var f = grunt.file.read(p, {encoding: null}); 23 | var type = mime.lookup(image); 24 | var data = 'data:' + type + ';base64,' + f.toString('base64'); 25 | cache[image] = data; 26 | grunt.log.writeln('File "' + image + '" converted into data URI.'); 27 | } 28 | return s1 + data + s2 + image; 29 | }); 30 | 31 | if (matched){ 32 | grunt.file.write(f, newFile); 33 | grunt.log.writeln('File "' + f + '" modified with embedded images.'); 34 | } else { 35 | grunt.log.writeln('File "' + f + '" not modified.'); 36 | } 37 | }); 38 | 39 | }); 40 | 41 | }; -------------------------------------------------------------------------------- /tasks/templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var hogan = require('hogan.js'); 5 | 6 | module.exports = function(grunt){ 7 | 8 | grunt.registerMultiTask('templates', 'Compile all templates', function(){ 9 | 10 | this.files.forEach(function(f){ 11 | var code = '(function(t){\n' 12 | + '\tTEMPLATES = {\n'; 13 | code += f.src.map(function(filepath){ 14 | var mustache = grunt.file.read(filepath); 15 | var key = path.basename(filepath, path.extname(filepath)); 16 | // Clean up some spaces 17 | mustache = mustache.replace(/[\r\n\t]+/g, ''); 18 | return "\t\t'" + key + "': new t(" + hogan.compile(mustache, {asString: true}) + ')'; 19 | }).join(',\n'); 20 | code += '\n\t}\n' 21 | + '})(Hogan.Template);'; 22 | 23 | grunt.file.write(f.dest, code); 24 | grunt.log.writeln('File "' + f.dest + '" created.'); 25 | }); 26 | 27 | }); 28 | 29 | }; --------------------------------------------------------------------------------