├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── app-dmg.json ├── app ├── client │ ├── css │ │ ├── highlight.css │ │ └── index.css │ ├── js │ │ ├── app.js │ │ ├── controllers.js │ │ ├── directives.js │ │ ├── filters.js │ │ └── services.js │ └── templates │ │ ├── app.html │ │ ├── header.html │ │ └── search.html └── server │ ├── connection.js │ ├── fetcher.js │ ├── getport.js │ ├── imageSandBoxer.js │ ├── sandboxer.js │ ├── searcher.js │ └── server.js ├── bower.json ├── index.html ├── index.js ├── license ├── package.json ├── readme.md └── resources ├── BG.png ├── BG.tiff ├── icon.hqx ├── icon.icns ├── icon.ico └── icon.png /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/client/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | app/client/lib 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "curly": true, 6 | "immed": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "undef": true, 10 | "unused": "vars", 11 | "strict": true 12 | } 13 | -------------------------------------------------------------------------------- /app-dmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "The Jackal of Javascript", 3 | "icon": "./resources/icon.icns", 4 | "background": "./resources/BG.tiff", 5 | "icon-size": 80, 6 | "contents": [{ 7 | "x": 448, 8 | "y": 344, 9 | "type": "link", 10 | "path": "/Applications" 11 | }, { 12 | "x": 192, 13 | "y": 334, 14 | "type": "file", 15 | "path": "./build/mac/The Jackal of Javascript.app" 16 | }] 17 | } 18 | -------------------------------------------------------------------------------- /app/client/css/highlight.css: -------------------------------------------------------------------------------- 1 | /*http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/styles/github.min.css*/ 2 | 3 | .hljs, { 4 | display: block; 5 | overflow-x: auto; 6 | padding: 0.5em; 7 | color: #333; 8 | background: #f8f8f8; 9 | -webkit-text-size-adjust: none; 10 | -webkit-font-smoothing: auto; 11 | font-size: 14px; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-template_comment, 16 | .diff .hljs-header, 17 | .hljs-javadoc { 18 | color: #998; 19 | font-style: italic 20 | } 21 | 22 | .hljs-keyword, 23 | .css .rule .hljs-keyword, 24 | .hljs-winutils, 25 | .javascript .hljs-title, 26 | .nginx .hljs-title, 27 | .hljs-subst, 28 | .hljs-request, 29 | .hljs-status { 30 | color: #333; 31 | font-weight: bold 32 | } 33 | 34 | .hljs-number, 35 | .hljs-hexcolor, 36 | .ruby .hljs-constant { 37 | color: #008080 38 | } 39 | 40 | .hljs-string, 41 | .hljs-tag .hljs-value, 42 | .hljs-phpdoc, 43 | .hljs-dartdoc, 44 | .tex .hljs-formula { 45 | color: #d14 46 | } 47 | 48 | .hljs-title, 49 | .hljs-id, 50 | .scss .hljs-preprocessor { 51 | color: #900; 52 | font-weight: bold 53 | } 54 | 55 | .javascript .hljs-title, 56 | .hljs-list .hljs-keyword, 57 | .hljs-subst { 58 | font-weight: normal 59 | } 60 | 61 | .hljs-class .hljs-title, 62 | .hljs-type, 63 | .vhdl .hljs-literal, 64 | .tex .hljs-command { 65 | color: #458; 66 | font-weight: bold 67 | } 68 | 69 | .hljs-tag, 70 | .hljs-tag .hljs-title, 71 | .hljs-rules .hljs-property, 72 | .django .hljs-tag .hljs-keyword { 73 | color: #000080; 74 | font-weight: normal 75 | } 76 | 77 | .hljs-attribute, 78 | .hljs-variable, 79 | .lisp .hljs-body { 80 | color: #008080 81 | } 82 | 83 | .hljs-regexp { 84 | color: #009926 85 | } 86 | 87 | .hljs-symbol, 88 | .ruby .hljs-symbol .hljs-string, 89 | .lisp .hljs-keyword, 90 | .clojure .hljs-keyword, 91 | .scheme .hljs-keyword, 92 | .tex .hljs-special, 93 | .hljs-prompt { 94 | color: #990073 95 | } 96 | 97 | .hljs-built_in { 98 | color: #0086b3 99 | } 100 | 101 | .hljs-preprocessor, 102 | .hljs-pragma, 103 | .hljs-pi, 104 | .hljs-doctype, 105 | .hljs-shebang, 106 | .hljs-cdata { 107 | color: #999; 108 | font-weight: bold 109 | } 110 | 111 | .hljs-deletion { 112 | background: #fdd 113 | } 114 | 115 | .hljs-addition { 116 | background: #dfd 117 | } 118 | 119 | .diff .hljs-change { 120 | background: #0086b3 121 | } 122 | 123 | .hljs-chunk { 124 | color: #aaa 125 | } 126 | -------------------------------------------------------------------------------- /app/client/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-family: 'Open Sans', sans-serif !important; 4 | } 5 | 6 | .menuBtn { 7 | background-color: transparent; 8 | border: none; 9 | margin-left: 16px; 10 | outline: none; 11 | } 12 | 13 | md-list .md-button { 14 | color: inherit; 15 | text-align: left; 16 | width: 100%; 17 | } 18 | 19 | /* Using Data-URI converted from svg until becomes available 20 | https://github.com/google/material-design-icons 21 | */ 22 | .menuBtn { 23 | background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGcgaWQ9IkhlYWRlciI+CiAgICA8Zz4KICAgICAgICA8cmVjdCB4PSItNjE4IiB5PSItMjIzMiIgZmlsbD0ibm9uZSIgd2lkdGg9IjE0MDAiIGhlaWdodD0iMzYwMCIvPgogICAgPC9nPgo8L2c+CjxnIGlkPSJMYWJlbCI+CjwvZz4KPGcgaWQ9Ikljb24iPgogICAgPGc+CiAgICAgICAgPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CiAgICAgICAgPHBhdGggZD0iTTMsMThoMTh2LTJIM1YxOHogTTMsMTNoMTh2LTJIM1YxM3ogTTMsNnYyaDE4VjZIM3oiIHN0eWxlPSJmaWxsOiNmM2YzZjM7Ii8+CiAgICA8L2c+CjwvZz4KPGcgaWQ9IkdyaWQiIGRpc3BsYXk9Im5vbmUiPgogICAgPGcgZGlzcGxheT0iaW5saW5lIj4KICAgIDwvZz4KPC9nPgo8L3N2Zz4=) no-repeat center center; 24 | } 25 | 26 | .list { 27 | padding-top: 3px; 28 | padding-bottom: 3px; 29 | } 30 | 31 | img { 32 | display: block; 33 | clear: both; 34 | margin: 13.5px auto; 35 | box-shadow: 1px 1px 39px 9px rgba(0, 0, 0, 0.5); 36 | border: 0; 37 | height: auto; 38 | max-width: 63%; 39 | } 40 | 41 | .crayon-inline { 42 | border-width: 1px !important; 43 | border-color: #ccc !important; 44 | background: #f5f5f5 !important; 45 | border-style: solid !important; 46 | padding: 5px; 47 | border-radius: 4px; 48 | margin: 0 0 10px; 49 | color: #000; 50 | font-family: monospace; 51 | } 52 | 53 | blog-post { 54 | overflow-x: hidden; 55 | font-size: 18px; 56 | } 57 | 58 | pre { 59 | -webkit-font-smoothing: auto; 60 | font-size: 14px; 61 | background: #f5f5f5; 62 | padding: 10px; 63 | } 64 | 65 | a { 66 | color: #f45145; 67 | text-decoration: none; 68 | } 69 | 70 | .post-title { 71 | color: #f45145; 72 | font-size: 18px; 73 | font-weight: 600; 74 | } 75 | 76 | .mfb-component__button--main, 77 | .mfb-component__button--child { 78 | /* color: #F44336; 79 | background-color: white;*/ 80 | 81 | color: white; 82 | background-color: #F44336; 83 | } 84 | 85 | .no-pointer { 86 | cursor: default; 87 | } 88 | 89 | md-content { 90 | overflow-x: hidden; 91 | } 92 | 93 | md-whiteframe { 94 | background: #fff; 95 | margin: 20px; 96 | padding: 20px; 97 | } 98 | 99 | .center-block { 100 | margin: 0 auto; 101 | display: block; 102 | } 103 | 104 | .mg-top-20p { 105 | margin-top: 20px; 106 | } 107 | 108 | .search-dialog { 109 | width: 100%; 110 | min-height: 333px; 111 | } 112 | 113 | .online { 114 | width: 30px; 115 | height: 30px; 116 | background: green; 117 | border-radius: 50%; 118 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); 119 | right: 0; 120 | position: absolute; 121 | margin-right: 30px; 122 | cursor: pointer; 123 | } 124 | 125 | .offline { 126 | width: 30px; 127 | height: 30px; 128 | background: orange; 129 | border-radius: 50%; 130 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12); 131 | right: 0; 132 | position: absolute; 133 | margin-right: 30px; 134 | cursor: pointer; 135 | } 136 | 137 | .hide { 138 | display: none; 139 | } 140 | 141 | .re-pos-loader { 142 | position: absolute; 143 | margin-right: 72px; 144 | right: 0px; 145 | top: 7px; 146 | } 147 | -------------------------------------------------------------------------------- /app/client/js/app.js: -------------------------------------------------------------------------------- 1 | // download socket.io/socket.io.js via JS, as we do not know the port 2 | var socketScript = document.createElement('script'); 3 | socketScript.setAttribute('type', 'text/javascript'); 4 | socketScript.setAttribute('src', 'http://localhost:' + window.serverPort + '/socket.io/socket.io.js'); 5 | document.getElementsByTagName('head').item(0).appendChild(socketScript); 6 | 7 | // wait for the socket.io.js to be downloaded 8 | // then bootstrap Angular Manually 9 | // -> We need socket.io right from the word go! 10 | 11 | socketScript.onload = function() { 12 | angular.bootstrap(document, ["OfflineViewer"]); 13 | } 14 | 15 | angular.module('OfflineViewer', ['ngMaterial', 'ngRoute', 'hljs', 'socket.io', 'ng-mfb', 'OfflineViewer.controllers', 'OfflineViewer.filters', 'OfflineViewer.directives']) 16 | 17 | .config(['$routeProvider', '$mdThemingProvider', '$socketProvider', 18 | function($routeProvider, $mdThemingProvider, $socketProvider) { 19 | 20 | $socketProvider.setConnectionUrl('http://localhost:' + window.serverPort); 21 | 22 | $mdThemingProvider.theme('default') 23 | .primaryPalette('red') 24 | .accentPalette('orange'); 25 | 26 | $routeProvider 27 | .when('/', { 28 | templateUrl: 'app/client/templates/App.html', 29 | controller: 'AppCtrl' 30 | }) 31 | 32 | .otherwise({ 33 | redirectTo: '/' 34 | }); 35 | } 36 | ]) 37 | .run(['$rootScope', '$window', function($rootScope, $window) { 38 | 39 | $rootScope.inProgress = true; 40 | 41 | $rootScope.onLine = $window.navigator.onLine; 42 | 43 | $window.addEventListener('online', function() { 44 | $rootScope.onLine = true; 45 | $rootScope.$broadcast('viewer-online'); 46 | }); 47 | 48 | $window.addEventListener('offline', function() { 49 | $rootScope.onLine = false; 50 | $rootScope.$broadcast('viewer-offline'); 51 | }); 52 | 53 | }]) 54 | -------------------------------------------------------------------------------- /app/client/js/controllers.js: -------------------------------------------------------------------------------- 1 | angular.module('OfflineViewer.controllers', []) 2 | 3 | 4 | .controller('HeaderCtrl', ['$scope', '$mdSidenav', '$rootScope', function($scope, $mdSidenav, $rootScope) { 5 | $scope.status = $rootScope.onLine; // True: Online | False: Offline 6 | 7 | $scope.toggleSidenav = function(menuId) { 8 | $mdSidenav(menuId).toggle(); 9 | }; 10 | 11 | $scope.$on('viewer-online', function() { 12 | $scope.status = true; 13 | $scope.$apply(); 14 | }); 15 | 16 | $scope.$on('viewer-offline', function() { 17 | $scope.status = false; 18 | $scope.$apply(); 19 | }); 20 | }]) 21 | 22 | .controller('AppCtrl', ['$scope', '$socket', '$mdDialog', '$rootScope', '$window', 23 | function($scope, $socket, $mdDialog, $rootScope, $window) { 24 | // recheck again.. sometimes eventhough the wifi 25 | // is disconnected, the navigator thinks it is online! 26 | 27 | $scope.status = $rootScope.onLine = $window.navigator.onLine; 28 | 29 | $scope.posts = []; 30 | 31 | if (!$rootScope.onLine) { 32 | $mdDialog.show( 33 | $mdDialog.alert() 34 | .parent(angular.element(document.body)) 35 | .title('You are offline!') 36 | .content('I have noticed that you are offline, I need internet access for a while to download the posts. If you do not see any posts after sometime, launch the viewer after connecting to the internet. Prior saved posts will be accessible from the menu. ') 37 | .ariaLabel('You are offline') 38 | .ok('Got it!') 39 | ); 40 | } 41 | 42 | $socket.emit('load', $rootScope.onLine); 43 | 44 | $socket.on('loaded', function(posts) { 45 | $scope.posts = $scope.posts.concat(posts); 46 | $rootScope.inProgress = false; 47 | }); 48 | 49 | $scope.showPost = function(post) { 50 | $scope.post = post.content; 51 | } 52 | 53 | $scope.openExternal = function(url) { 54 | var confirm = $mdDialog.confirm() 55 | .parent(angular.element(document.body)) 56 | .title('Open the link?') 57 | .content('Are you sure you want to open ' + url) 58 | .ok('Yeah! Sure!') 59 | .cancel('No Thanks!') 60 | 61 | $mdDialog.show(confirm).then(function() { 62 | require('shell').openExternal(url); 63 | }, function() { 64 | // noop 65 | }); 66 | } 67 | 68 | $scope.$on('viewer-online', function() { 69 | $scope.status = true; 70 | $socket.emit('load', $scope.status); 71 | $scope.$apply(); 72 | }); 73 | 74 | $scope.$on('viewer-offline', function() { 75 | $scope.status = false; 76 | $scope.$apply(); 77 | }); 78 | 79 | 80 | $scope.$on('showSearchPost', function($event, post) { 81 | $scope.showPost(post); 82 | }); 83 | } 84 | ]) 85 | 86 | .controller('SearchCtrl', ['$scope', '$socket', '$mdDialog', function($scope, $socket, $mdDialog) { 87 | 88 | $scope.SearchBox = function() { 89 | $mdDialog.show({ 90 | controller: SearchDialogCtrl, 91 | templateUrl: 'app/client/templates/search.html', 92 | }); 93 | } 94 | 95 | function SearchDialogCtrl($scope, $mdDialog, $socket, $rootScope) { 96 | 97 | $scope.results = $rootScope.results; 98 | 99 | $scope.hide = function() { 100 | $mdDialog.hide(); 101 | }; 102 | 103 | $scope.search = function($event) { 104 | if ($event.which === 13) { 105 | $scope.searching = true; 106 | $socket.emit('search', $scope.searchText); 107 | } 108 | } 109 | 110 | $scope.showPost = function(post) { 111 | $scope.hide(); 112 | $rootScope.$broadcast('showSearchPost', post); 113 | } 114 | 115 | $socket.on('results', function(results) { 116 | $scope.searching = false; 117 | $scope.results = results; 118 | $rootScope.results = results; 119 | }); 120 | } 121 | 122 | }]) 123 | -------------------------------------------------------------------------------- /app/client/js/directives.js: -------------------------------------------------------------------------------- 1 | angular.module('OfflineViewer.directives', []) 2 | 3 | .directive('blogPost', ['$compile', '$parse', function($compile, $parse) { 4 | 5 | //http://stackoverflow.com/a/21374642/1015046 6 | var template = "
{{post.content}}
" 7 | return { 8 | restrict: 'E', 9 | link: function(s, e, a) { 10 | s.$watch('post', function(val) { 11 | if (val) { 12 | e.html($parse(a.post)(s)); 13 | $compile(e.contents())(s); 14 | } 15 | }); 16 | } 17 | }; 18 | }]) 19 | -------------------------------------------------------------------------------- /app/client/js/filters.js: -------------------------------------------------------------------------------- 1 | angular.module('OfflineViewer.filters', []) 2 | 3 | .filter('toHTML', function($sce) { 4 | return function(input) { 5 | return $sce.trustAsHtml(input); 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /app/client/js/services.js: -------------------------------------------------------------------------------- 1 | // -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-// 2 | // https://github.com/tiagocparra/angularJS-socketIO// 3 | // -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-// 4 | 5 | /* 6 | *http://code.tutsplus.com/tutorials/more-responsive-single-page-applications-with-angularjs-socketio-creating-the-library--cms-21738 7 | */ 8 | 9 | var module = angular.module('socket.io', []); 10 | 11 | module.provider('$socket', function $socketProvider() { 12 | 13 | var ioUrl = ''; 14 | var ioConfig = {}; 15 | 16 | function setOption(name, value, type) { 17 | if (typeof value != type) { 18 | throw new TypeError("'" + name + "' must be of type '" + type + "'"); 19 | } 20 | ioConfig[name] = value; 21 | } 22 | 23 | this.setResource = function setResource(value) { 24 | setOption('resource', value, 'string'); 25 | }; 26 | this.setConnectTimeout = function setConnectTimeout(value) { 27 | setOption('connect timeout', value, 'number'); 28 | }; 29 | this.setTryMultipleTransports = function setTryMultipleTransports(value) { 30 | setOption('try multiple transports', value, 'boolean'); 31 | }; 32 | this.setReconnect = function setReconnect(value) { 33 | setOption('reconnect', value, 'boolean'); 34 | }; 35 | this.setReconnectionDelay = function setReconnectionDelay(value) { 36 | setOption('reconnection delay', value, 'number'); 37 | }; 38 | this.setReconnectionLimit = function setReconnectionLimit(value) { 39 | setOption('reconnection limit', value, 'number'); 40 | }; 41 | this.setMaxReconnectionAttempts = function setMaxReconnectionAttempts(value) { 42 | setOption('max reconnection attempts', value, 'number'); 43 | }; 44 | this.setSyncDisconnectOnUnload = function setSyncDisconnectOnUnload(value) { 45 | setOption('sync disconnect on unload', value, 'boolean'); 46 | }; 47 | this.setAutoConnect = function setAutoConnect(value) { 48 | setOption('auto connect', value, 'boolean'); 49 | }; 50 | this.setFlashPolicyPort = function setFlashPolicyPort(value) { 51 | setOption('flash policy port', value, 'number') 52 | }; 53 | this.setForceNewConnection = function setForceNewConnection(value) { 54 | setOption('force new connection', value, 'boolean'); 55 | }; 56 | this.setConnectionUrl = function setConnectionUrl(value) { 57 | if ('string' !== typeof value) { 58 | throw new TypeError("setConnectionUrl error: value must be of type 'string'"); 59 | } 60 | ioUrl = value; 61 | } 62 | 63 | this.$get = function $socketFactory($rootScope) { 64 | 65 | var socket = io(ioUrl, ioConfig); 66 | 67 | return { 68 | on: function on(event, callback) { 69 | socket.on(event, function() { 70 | //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments 71 | var args = arguments; 72 | // $apply faz com que a callback possa invocar variaveis 73 | // em $scope que tenham sido declaradas pela 74 | // directiva ng-model ou {{}} 75 | $rootScope.$apply(function() { 76 | // este callback.apply regista a função callback que 77 | // poderá conter referencias à variavel socket 78 | callback.apply(socket, args); 79 | }); 80 | }); 81 | }, 82 | off: function off(event, callback) { 83 | if (typeof callback == 'function') { 84 | //neste caso o callback nao tem acesso a $scope nem à 85 | //scope definida pela variavel socket 86 | socket.removeListener(event, callback); 87 | } else { 88 | socket.removeAllListeners(event); 89 | } 90 | }, 91 | emit: function emit(event, data, callback) { 92 | if (typeof callback == 'function') { 93 | socket.emit(event, data, function() { 94 | var args = arguments; 95 | $rootScope.$apply(function() { 96 | callback.apply(socket, args); 97 | }); 98 | }); 99 | } else { 100 | socket.emit(event, data); 101 | } 102 | } 103 | }; 104 | }; 105 | }); 106 | -------------------------------------------------------------------------------- /app/client/templates/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | All Posts 4 | 5 | 6 |
7 | 8 |
9 |
10 |
11 |
12 | 13 | 14 |
15 |
Click on the Menu to start reading posts!
16 |
17 | Waiting for First Page Posts to Load (This may take some time).. 18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /app/client/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |

The Jackal of Javascript

6 |
7 | 8 |
9 |
10 |
11 | 12 | You are online! 13 | 14 |
15 |
16 | 17 | You are offline! 18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /app/client/templates/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 | Search Posts 8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | No results found!! 23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /app/server/connection.js: -------------------------------------------------------------------------------- 1 | var db = require('diskdb'); 2 | db = db.connect(__dirname + '/db', ['posts', 'meta']); 3 | 4 | module.exports = db; 5 | -------------------------------------------------------------------------------- /app/server/fetcher.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var sandboxer = require('./sandboxer'); 3 | var imageSandBoxer = require('./imageSandBoxer'); 4 | var db = require('./connection'); 5 | 6 | var baseUrl = 'https://public-api.wordpress.com/rest/v1.1/sites/thejackalofjavascript.com/posts', 7 | page = 1, 8 | perPage = 20, 9 | length = 0, 10 | total, inProgress = false; 11 | 12 | module.exports = function fetcher(isOnline, callback, skip) { 13 | var posts = db.posts.find(); 14 | var total = db.meta.findOne({ 15 | 'key': 'found' 16 | }); 17 | 18 | if (!total) { 19 | total = { 20 | 'key': 'found', 21 | 'total': -1 22 | } 23 | db.meta.save(total); 24 | } 25 | 26 | total = total.total; 27 | length = posts.length; 28 | if (length > 0 && !skip) { 29 | // if all posts are downloaded, we will send them back 30 | // with out making a call to the JSON server 31 | 32 | // Feature : If you want, you can add a property to the 33 | // meta collection, named `lastUpdate` 34 | // > And if all the posts are downloaded, you can dispatch them 35 | // as usual & then check the `lastUpdate` and if it is > 1 day or 1 week, 36 | // download all the posts again to check for any updates 37 | 38 | if (length == total) { 39 | return sendByParts(posts.splice(0, perPage)); 40 | } else { 41 | sendByParts(posts.splice(0, perPage)); 42 | 43 | if (total > length) { 44 | page = length / perPage + 1; 45 | fetcher(isOnline, callback, true); 46 | } 47 | return false; 48 | } 49 | 50 | } 51 | // clean up old posts 52 | if (page === 1) { 53 | db.posts.remove(); 54 | db.loadCollections(['posts']); 55 | } 56 | 57 | // make request only if we are online! 58 | if (isOnline) { 59 | request(baseUrl + '?page=' + page, 60 | function(error, response, body) { 61 | if (!error && response.statusCode == 200) { 62 | body = JSON.parse(body); 63 | posts = body.posts; 64 | if (posts.length > 0) { 65 | 66 | // update the found count 67 | db.meta.update({ 68 | 'key': 'found' 69 | }, { 70 | 'total': body.found 71 | }, { 72 | upsert: true 73 | }); 74 | 75 | // sandbox the pages 76 | sandboxer(posts, function(posts) { 77 | posts = posts.reverse(); 78 | db.posts.save(posts); 79 | posts = posts.reverse(); 80 | callback(posts); 81 | page++; 82 | fetcher(isOnline, callback, true); 83 | }); 84 | } else { 85 | page = 1; 86 | 87 | // now that we have all the posts, 88 | // we will start sandboxing the images 89 | // this is the process of taking a image URL 90 | // and converting it to base64 91 | // making this app a true offline viewer! 92 | 93 | imageSandBoxer(isOnline, function() { 94 | // ALL Done! 95 | //console.log('Sandboxing Images done!!') 96 | }); 97 | 98 | // Videos are a bit complex, so left it out :D 99 | } 100 | } 101 | }); 102 | } 103 | 104 | function sendByParts(_posts) { 105 | if (_posts) { 106 | // send only 20 posts per 1 second 107 | setTimeout(function() { 108 | callback(_posts); 109 | return sendByParts(posts.splice(0, perPage)); 110 | }, 1000); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/server/getport.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/mikeal/1840641 2 | var net = require('net'); 3 | var portrange = 45032 4 | 5 | function getPort(cb) { 6 | var port = portrange 7 | portrange += 1 8 | 9 | var server = net.createServer(); 10 | server.listen(port, function(err) { 11 | server.once('close', function() { 12 | cb(port); 13 | }); 14 | server.close(); 15 | }); 16 | 17 | server.on('error', function(err) { 18 | getPort(cb); 19 | }); 20 | } 21 | 22 | module.exports = getPort; 23 | -------------------------------------------------------------------------------- /app/server/imageSandBoxer.js: -------------------------------------------------------------------------------- 1 | var db = require('./connection.js'); 2 | var cheerio = require('cheerio'); 3 | 4 | // http://stackoverflow.com/a/17133012/1015046 5 | var request = require('request').defaults({ 6 | encoding: null 7 | }); 8 | 9 | var $imgs, posts, $; 10 | 11 | module.exports = function imageSandBoxer(isOnline, callback) { 12 | if (!isOnline) return; 13 | 14 | posts = db.posts.find(); 15 | processPost(posts.shift(), callback); 16 | } 17 | 18 | function processPost(post, callback) { 19 | if (post) { 20 | $ = cheerio.load(post.content); 21 | $imgs = $('img').toArray(); 22 | sandBoxImage($imgs.shift(), post, callback); 23 | } else { 24 | callback(); 25 | } 26 | } 27 | 28 | function sandBoxImage($img, post, callback) { 29 | if ($img) { 30 | request.get($img.attribs.src, function(error, response, body) { 31 | if (!error && response.statusCode == 200) { 32 | data = "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64'); 33 | $img.attribs.src = data; 34 | sandBoxImage($imgs.shift(), post, callback); 35 | } 36 | }); 37 | 38 | } else { 39 | post.content = $.html(); 40 | var res = db.posts.update({ 41 | "ID": post.ID 42 | }, post); 43 | return processPost(posts.shift(), callback); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/server/sandboxer.js: -------------------------------------------------------------------------------- 1 | var cheerio = require('cheerio'); 2 | var _sandBoxedPosts = [], 3 | _posts = [], 4 | $imgs; 5 | 6 | module.exports = function(posts, callback) { 7 | _sandBoxedPosts = []; 8 | _posts = posts; 9 | process(_posts.shift(), function() { 10 | callback(_sandBoxedPosts); 11 | }); 12 | } 13 | 14 | function process(post, cb) { 15 | if (post) { 16 | var $ = cheerio.load(post.content); 17 | 18 | // anchor tags 19 | $('a').each(function(i, e) { 20 | 21 | // sandbox links 22 | e.attribs['ng-click'] = 'openExternal(\'' + e.attribs.href + '\')'; 23 | 24 | // remove onclick functions 25 | // I use GA to track clicks in my blog 26 | delete e.attribs.onclick; 27 | delete e.attribs.target; 28 | e.attribs.href = 'javascript:'; 29 | 30 | var c = e.children; 31 | 32 | if (c.length == 1) { 33 | // pure anchor tag 34 | 35 | } else { 36 | // clean up image tags 37 | if (c.length && c.length > 0 && c[0].name == 'img') { 38 | var i = c[0]; 39 | i.attribs.src = i.attribs["data-lazy-src"]; 40 | i.attribs.src = i.attribs.src.split('?resize')[0]; 41 | delete i.attribs["data-lazy-src"]; 42 | // remove the parent anchor link for 43 | // a image tag 44 | delete e.attribs['ng-click']; 45 | e.attribs['class'] = 'no-pointer'; 46 | } 47 | } 48 | 49 | }); 50 | 51 | // pre tags 52 | $('pre').each(function(i, e) { 53 | e.attribs.hljs = 'true'; 54 | e.attribs['no-escape'] = 'true'; 55 | }); 56 | 57 | // iframes 58 | $('iframe').each(function(i, e) { 59 | var src = e.attribs.src; 60 | if (src.indexOf('youtube') > 0) { 61 | if (src.indexOf('http') != 0) { 62 | e.attribs.src = 'http:' + e.attribs.src; 63 | } 64 | var p = $(e).parents('p')[0]; 65 | p.attribs['ng-class'] = '{hide : !status}'; 66 | } 67 | }); 68 | 69 | $('img').each(function(i, e) { 70 | e.attribs.src = e.attribs.src.split('?resize')[0]; 71 | }) 72 | 73 | post.content = $.html(); 74 | _sandBoxedPosts.push(post); 75 | return process(_posts.shift(), cb); 76 | } else { 77 | cb(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/server/searcher.js: -------------------------------------------------------------------------------- 1 | var db = require('./connection.js'); 2 | 3 | module.exports = function searcher(q, cb) { 4 | 5 | //NO search API for disk DB :( 6 | var posts = db.posts.find(); 7 | var results = []; 8 | 9 | for (var i = 0; i < posts.length; i++) { 10 | 11 | var p = posts[i]; 12 | 13 | if (p.title.indexOf(q) >= 0 || p.content.indexOf(q) >= 0) { 14 | results.push(p); 15 | } 16 | 17 | }; 18 | 19 | cb(results); 20 | } 21 | -------------------------------------------------------------------------------- /app/server/server.js: -------------------------------------------------------------------------------- 1 | var getport = require('./getport'); 2 | var fetcher = require('./fetcher'); 3 | var searcher = require('./searcher'); 4 | 5 | module.exports = function(callback) { 6 | // get an unused port! 7 | getport(function(port) { 8 | 9 | var io = require('socket.io')(port); 10 | 11 | io.sockets.on('connection', function(socket) { 12 | 13 | socket.on('load', function(isOnline) { 14 | fetcher(isOnline, function(posts) { 15 | socket.emit('loaded', posts); 16 | }); 17 | }); 18 | 19 | socket.on('search', function(q) { 20 | searcher(q, function(results) { 21 | socket.emit('results', results); 22 | }); 23 | }); 24 | 25 | }); 26 | 27 | callback(port); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-offline-viewer", 3 | "main": "index.js", 4 | "version": "0.1.0", 5 | "authors": [ 6 | "Arvind Ravulavaru " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "resolutions": { 17 | "angular": "1.4.0" 18 | }, 19 | "dependencies": { 20 | "angular-material": "~0.9.4", 21 | "angular-route": "~1.4.0", 22 | "roboto-fontface": "~0.4.2", 23 | "open-sans-fontface": "~1.4.0", 24 | "angular-highlightjs": "~0.4.1", 25 | "ng-mfb": "~0.6.0", 26 | "ionicons": "~2.0.1", 27 | "jquery": "~2.1.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The Jackal of Javascript | Offline Viewer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const app = require('app'); 3 | const BrowserWindow = require('browser-window'); 4 | const Menu = require('menu'); 5 | 6 | // report crashes to the Electron project 7 | require('crash-reporter').start(); 8 | 9 | // prevent window being GC'd 10 | let mainWindow = null; 11 | 12 | app.on('ready', function() { 13 | mainWindow = new BrowserWindow({ 14 | // https://github.com/atom/electron/blob/master/docs/api/browser-window.md 15 | 'min-width': 1000, 16 | 'min-height': 400, 17 | width: 1200, 18 | height: 600, 19 | center: true, 20 | resizable: true 21 | }); 22 | 23 | mainWindow.loadUrl(`file://${__dirname}/index.html`); 24 | 25 | mainWindow.on('closed', function() { 26 | // deref the window 27 | // for multiple windows store them in an array 28 | mainWindow = null; 29 | }); 30 | 31 | // uncomment the below line to open devetools 32 | //mainWindow.openDevTools(); 33 | }); 34 | 35 | // does not work when placed on top for some reason. 36 | app.on('window-all-closed', function() { 37 | app.quit(); 38 | }); 39 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Arvind Ravulavaru (http://thejackalofjavascript.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-offline-viewer", 3 | "productName": "WordpressOfflineViewer", 4 | "version": "0.1.0", 5 | "description": "Electron, WordPress & Angular Material – An Offline Viewer", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "repository": "https://github.com/arvindr21/wordpress-offline-viewer", 9 | "author": { 10 | "name": "Arvind Ravulavaru", 11 | "email": "arvind.ravulavaru@gmail.com", 12 | "url": "thejackalofjavascript.com" 13 | }, 14 | "engines": { 15 | "node": ">=0.10.0" 16 | }, 17 | "scripts": { 18 | "start": "electron .", 19 | "clean" : "trash --force build", 20 | "clean-mac": "trash --force build/mac release", 21 | "clean-linux": "trash --force build/linux", 22 | "clean-win": "trash --force build/win", 23 | "build-mac": "npm run clean-mac && electron-packager . 'The Jackal of Javascript' --platform=darwin --arch=x64 --version=0.25.2 --icon ./resources/icon.icns --out ./build/mac --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git", 24 | "build-linux": "npm run clean-linux && electron-packager . 'The Jackal of Javascript' --platform=linux --arch=x64 --version=0.25.2 --icon ./resources/icon.icns --out ./build/linux --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git", 25 | "build-win": "npm run clean-win && electron-packager . 'The Jackal of Javascript' --platform=win32 --arch=ia32 --version=0.25.2 --out ./build/win --prune --ignore=node_modules/electron-prebuilt --ignore=node_modules/electron-packager --ignore=node_modules/trash --ignore=.git", 26 | "build" : "npm run clean && npm run build-win && npm run build-linux && npm run build-mac", 27 | "release-mac": "npm run clean-mac && npm run build-mac && mkdir release && cd release && appdmg ../app-dmg.json 'The Javascript of Javascript.dmg'" 28 | }, 29 | "keywords": [ 30 | "electron-app", 31 | "offline", 32 | "viewer" 33 | ], 34 | "devDependencies": { 35 | "appdmg": "^0.3.1", 36 | "electron-packager": "^4.1.1", 37 | "electron-prebuilt": "^0.25.2", 38 | "trash": "^1.4.1" 39 | }, 40 | "dependencies": { 41 | "cheerio": "^0.19.0", 42 | "diskdb": "^0.1.14", 43 | "request": "^2.55.0", 44 | "socket.io": "^1.3.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [Electron, Wordpress & Angular Material - An Offline Viewer](http://thejackalofjavascript.com/electron-wordpress-angular-material-an-offline-viewer) 2 | -------------------------------------------------------------------------------- /resources/BG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arvindr21/wordpress-offline-viewer/d79bcf2e076acdfd5200277de8aa997193e5e2fc/resources/BG.png -------------------------------------------------------------------------------- /resources/BG.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arvindr21/wordpress-offline-viewer/d79bcf2e076acdfd5200277de8aa997193e5e2fc/resources/BG.tiff -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arvindr21/wordpress-offline-viewer/d79bcf2e076acdfd5200277de8aa997193e5e2fc/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arvindr21/wordpress-offline-viewer/d79bcf2e076acdfd5200277de8aa997193e5e2fc/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arvindr21/wordpress-offline-viewer/d79bcf2e076acdfd5200277de8aa997193e5e2fc/resources/icon.png --------------------------------------------------------------------------------