├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.md ├── assets-linux ├── README.md ├── after-install.sh ├── after-remove.sh ├── icons │ ├── 48 │ │ └── starter.png │ ├── 256 │ │ └── starter.png │ └── scalable │ │ └── starter.svg └── starter.desktop ├── assets-osx ├── README.md ├── dmg.json ├── dmg_bg.png ├── dmg_bg@2x.png ├── dmg_icon.icns └── icon.icns ├── assets-windows ├── README.md ├── icon.ico └── installer.nsi ├── design.sketch ├── gulpfile.coffee ├── package.json ├── screenshot.jpg ├── src ├── app.css ├── app.html ├── app.js ├── components │ ├── dispatcher.js │ ├── menus.js │ ├── platform.js │ ├── settings.js │ ├── updater.js │ └── window-behaviour.js ├── images │ ├── icon.png │ ├── icon_menubar.tiff │ ├── icon_menubar_alert.tiff │ ├── icon_tray.png │ └── icon_tray_alert.png └── package.json └── version.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /cache/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | - First release 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexandru Rosianu 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NW.js Starter 2 | 3 | [![Join the chat at https://gitter.im/Aluxian/nwjs-starter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Aluxian/nwjs-starter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/aluxian) 5 | 6 | This is a sample project to get you up and running with [NW.js](http://nwjs.io/). 7 | 8 | If you need help, read my tutorial [here](http://blog.aluxian.com/how-to-create-cross-platform-desktop-apps-with-nw-js/). This is a screenshot of the app that the tutorial guides you through, Klout: 9 | 10 | ![Cross-platform screenshot](screenshot.jpg) 11 | 12 | ## Features 13 | 14 | * native notifications 15 | * auto update 16 | * gulp tasks to 17 | * build each release 18 | * sign the OS X app 19 | * pack 20 | * Windows: nsis installer 21 | * OSX: dmg 22 | * Linux: deb package 23 | * upload to GitHub 24 | * remembers window position when closed 25 | * customizable settings in the right-click menu 26 | * run as menu bar app 27 | * launch on OS startup 28 | * open links in the browser 29 | 30 | What doesn't work: 31 | 32 | * tray icon on linux 33 | * rpm builds 34 | 35 | Not done yet: 36 | * Windows binary signing 37 | 38 | ## Pre-requisites 39 | 40 | # install gulp 41 | npm install -g gulp 42 | 43 | # install dependencies 44 | npm install 45 | 46 | * **wine**: If you're on OS X/Linux and want to build for Windows, you need [Wine](http://winehq.org/) installed. Wine is required in order 47 | to set the correct icon for the exe. If you don't have Wine, you can comment out the `winIco` field in `gulpfile`. 48 | * **makensis**: Required by the `pack:win32` task in `gulpfile` to create the Windows installer. 49 | * [**fpm**](https://github.com/jordansissel/fpm): Required by the `pack:linux{32|64}:deb` tasks in `gulpfile` to create the Linux installers. 50 | 51 | Quickly install this on OS X: 52 | 53 | brew install wine makensis 54 | sudo gem install fpm 55 | 56 | ## Build 57 | 58 | ### OS X: pack the app in a .dmg 59 | 60 | gulp pack:osx64 61 | 62 | ### Windows: create the installer 63 | 64 | gulp pack:win32 65 | 66 | ### Linux 32/64-bit: pack the app in a .deb 67 | 68 | gulp pack:linux{32|64}:deb 69 | 70 | The output is in `./dist`. Take a look at `gulpfile.coffee` for additional tasks. 71 | 72 | ## Tips 73 | 74 | Use the `--toolbar` parameter to quickly build the app with the toolbar on. E.g. `gulp build:win32 --toolbar`. 75 | 76 | Use `gulp build:win32 --noicon` to quickly build the Windows app without the icon, which normally takes some seconds. 77 | 78 | For OS X, use the `run:osx64` task to build the app and run it immediately. 79 | 80 | Use these to create the .tiff images for the OS X menu bar: 81 | 82 | $ tiffutil -cathidpicheck icon_menubar.png icon_menubar@2x.png -out icon_menubar.tiff 83 | $ tiffutil -cathidpicheck icon_menubar_alert.png icon_menubar_alert@2x.png -out icon_menubar_alert.tiff 84 | 85 | ## Contributions 86 | 87 | Contributions are welcome! For feature requests and bug reports please [submit an issue](https://github.com/Aluxian/nwjs-starter/issues). 88 | 89 | ## License 90 | 91 | The MIT License (MIT) 92 | 93 | Copyright (c) 2015 Alexandru Rosianu 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy 96 | of this software and associated documentation files (the "Software"), to deal 97 | in the Software without restriction, including without limitation the rights 98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 | copies of the Software, and to permit persons to whom the Software is 100 | furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in all 103 | copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 111 | SOFTWARE. 112 | -------------------------------------------------------------------------------- /assets-linux/README.md: -------------------------------------------------------------------------------- 1 | # Linux related assets 2 | 3 | - `after-install` and `after-remove` scripts 4 | - `starter.desktop` and icons for launchers 5 | -------------------------------------------------------------------------------- /assets-linux/after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | ln -sf /opt/starter/Starter /usr/local/bin/starter 5 | 6 | # Launcher icon 7 | desktop-file-install /opt/starter/starter.desktop 8 | -------------------------------------------------------------------------------- /assets-linux/after-remove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | rm -f /usr/local/bin/starter 5 | 6 | # Launcher files 7 | rm -f /usr/share/applications/starter.desktop 8 | -------------------------------------------------------------------------------- /assets-linux/icons/256/starter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-linux/icons/256/starter.png -------------------------------------------------------------------------------- /assets-linux/icons/48/starter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-linux/icons/48/starter.png -------------------------------------------------------------------------------- /assets-linux/icons/scalable/starter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon@svg 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets-linux/starter.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Name=Starter 5 | Icon=starter 6 | Exec=/opt/starter/Starter 7 | Comment=Starter 8 | Categories=GNOME;GTK;Development;Documentation; 9 | Terminal=false 10 | -------------------------------------------------------------------------------- /assets-osx/README.md: -------------------------------------------------------------------------------- 1 | # OS X related assets 2 | 3 | - `dmg.json` config file for node-appdmg 4 | -------------------------------------------------------------------------------- /assets-osx/dmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Starter", 3 | "icon": "dmg_icon.icns", 4 | "background": "dmg_bg.png", 5 | "icon-size": 80, 6 | "contents": [ 7 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 8 | { "x": 192, "y": 344, "type": "file", "path": "../build/Starter/osx64/Starter.app" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /assets-osx/dmg_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-osx/dmg_bg.png -------------------------------------------------------------------------------- /assets-osx/dmg_bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-osx/dmg_bg@2x.png -------------------------------------------------------------------------------- /assets-osx/dmg_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-osx/dmg_icon.icns -------------------------------------------------------------------------------- /assets-osx/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-osx/icon.icns -------------------------------------------------------------------------------- /assets-windows/README.md: -------------------------------------------------------------------------------- 1 | # Windows related assets 2 | 3 | - `installer.nsi` config file for nsis 4 | -------------------------------------------------------------------------------- /assets-windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/assets-windows/icon.ico -------------------------------------------------------------------------------- /assets-windows/installer.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI2.nsh" 2 | 3 | Name "Starter" 4 | BrandingText "aluxian.com" 5 | 6 | # set the icon 7 | !define MUI_ICON "icon.ico" 8 | 9 | # define the resulting installer's name: 10 | OutFile "..\dist\StarterSetup.exe" 11 | 12 | # set the installation directory 13 | InstallDir "$PROGRAMFILES\Starter\" 14 | 15 | # app dialogs 16 | !insertmacro MUI_PAGE_WELCOME 17 | !insertmacro MUI_PAGE_INSTFILES 18 | 19 | !define MUI_FINISHPAGE_RUN_TEXT "Start Starter" 20 | !define MUI_FINISHPAGE_RUN "$INSTDIR\Starter.exe" 21 | 22 | !insertmacro MUI_PAGE_FINISH 23 | !insertmacro MUI_LANGUAGE "English" 24 | 25 | # default section start 26 | Section 27 | 28 | # delete the installed files 29 | RMDir /r $INSTDIR 30 | 31 | # define the path to which the installer should install 32 | SetOutPath $INSTDIR 33 | 34 | # specify the files to go in the output path 35 | File /r ..\build\Starter\win32\* 36 | 37 | # create the uninstaller 38 | WriteUninstaller "$INSTDIR\Uninstall Starter.exe" 39 | 40 | # create shortcuts in the start menu and on the desktop 41 | CreateShortCut "$SMPROGRAMS\Starter.lnk" "$INSTDIR\Starter.exe" 42 | CreateShortCut "$SMPROGRAMS\Uninstall Starter.lnk" "$INSTDIR\Uninstall Starter.exe" 43 | CreateShortCut "$DESKTOP\Starter.lnk" "$INSTDIR\Starter.exe" 44 | 45 | SectionEnd 46 | 47 | # create a section to define what the uninstaller does 48 | Section "Uninstall" 49 | 50 | # delete the installed files 51 | RMDir /r $INSTDIR 52 | 53 | # delete the shortcuts 54 | Delete "$SMPROGRAMS\Starter.lnk" 55 | Delete "$SMPROGRAMS\Uninstall Starter.lnk" 56 | Delete "$DESKTOP\Starter.lnk" 57 | 58 | SectionEnd 59 | -------------------------------------------------------------------------------- /design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/design.sketch -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | shelljs = require 'shelljs' 3 | mergeStream = require 'merge-stream' 4 | runSequence = require 'run-sequence' 5 | manifest = require './package.json' 6 | $ = require('gulp-load-plugins')() 7 | 8 | # Remove directories used by the tasks 9 | gulp.task 'clean', -> 10 | shelljs.rm '-rf', './build' 11 | shelljs.rm '-rf', './dist' 12 | 13 | # Build for each platform; on OSX/Linux, you need Wine installed to build win32 (or remove winIco below) 14 | ['win32', 'osx64', 'linux32', 'linux64'].forEach (platform) -> 15 | gulp.task 'build:' + platform, -> 16 | if process.argv.indexOf('--toolbar') > 0 17 | shelljs.sed '-i', '"toolbar": false', '"toolbar": true', './src/package.json' 18 | 19 | gulp.src './src/**' 20 | .pipe $.nodeWebkitBuilder 21 | platforms: [platform] 22 | version: '0.12.2' 23 | winIco: if process.argv.indexOf('--noicon') > 0 then undefined else './assets-windows/icon.ico' 24 | macIcns: './assets-osx/icon.icns' 25 | macZip: true 26 | macPlist: 27 | NSHumanReadableCopyright: 'aluxian.com' 28 | CFBundleIdentifier: 'com.aluxian.starter' 29 | .on 'end', -> 30 | if process.argv.indexOf('--toolbar') > 0 31 | shelljs.sed '-i', '"toolbar": true', '"toolbar": false', './src/package.json' 32 | 33 | # Only runs on OSX (requires XCode properly configured) 34 | gulp.task 'sign:osx64', ['build:osx64'], -> 35 | shelljs.exec 'codesign -v -f -s "Alexandru Rosianu Apps" ./build/Starter/osx64/Starter.app/Contents/Frameworks/*' 36 | shelljs.exec 'codesign -v -f -s "Alexandru Rosianu Apps" ./build/Starter/osx64/Starter.app' 37 | shelljs.exec 'codesign -v --display ./build/Starter/osx64/Starter.app' 38 | shelljs.exec 'codesign -v --verify ./build/Starter/osx64/Starter.app' 39 | 40 | # Create a DMG for osx64; only works on OS X because of appdmg 41 | gulp.task 'pack:osx64', ['sign:osx64'], -> 42 | shelljs.mkdir '-p', './dist' # appdmg fails if ./dist doesn't exist 43 | shelljs.rm '-f', './dist/Starter.dmg' # appdmg fails if the dmg already exists 44 | 45 | gulp.src [] 46 | .pipe require('gulp-appdmg') 47 | source: './assets-osx/dmg.json' 48 | target: './dist/Starter.dmg' 49 | 50 | # Create a nsis installer for win32; must have `makensis` installed 51 | gulp.task 'pack:win32', ['build:win32'], -> 52 | shelljs.exec 'makensis ./assets-windows/installer.nsi' 53 | 54 | # Create packages for linux 55 | [32, 64].forEach (arch) -> 56 | ['deb', 'rpm'].forEach (target) -> 57 | gulp.task "pack:linux#{arch}:#{target}", ['build:linux' + arch], -> 58 | shelljs.rm '-rf', './build/linux' 59 | 60 | move_opt = gulp.src [ 61 | './assets-linux/starter.desktop' 62 | './assets-linux/after-install.sh' 63 | './assets-linux/after-remove.sh' 64 | './build/Starter/linux' + arch + '/**' 65 | ] 66 | .pipe gulp.dest './build/linux/opt/starter' 67 | 68 | move_png48 = gulp.src './assets-linux/icons/48/starter.png' 69 | .pipe gulp.dest './build/linux/usr/share/icons/hicolor/48x48/apps' 70 | 71 | move_png256 = gulp.src './assets-linux/icons/256/starter.png' 72 | .pipe gulp.dest './build/linux/usr/share/icons/hicolor/256x256/apps' 73 | 74 | move_svg = gulp.src './assets-linux/icons/scalable/starter.png' 75 | .pipe gulp.dest './build/linux/usr/share/icons/hicolor/scalable/apps' 76 | 77 | mergeStream move_opt, move_png48, move_png256, move_svg 78 | .on 'end', -> 79 | shelljs.cd './build/linux' 80 | 81 | port = if arch == 32 then 'i386' else 'x86_64' 82 | output = "../../dist/Starter_linux#{arch}.#{target}" 83 | 84 | shelljs.mkdir '-p', '../../dist' # it fails if the dir doesn't exist 85 | shelljs.rm '-f', output # it fails if the package already exists 86 | 87 | shelljs.exec "fpm -s dir -t #{target} -a #{port} --rpm-os linux -n starter --after-install ./opt/starter/after-install.sh --after-remove ./opt/starter/after-remove.sh --license MIT --category Chat --url \"https://example.com\" --description \"A sample NW.js app.\" -m \"Alexandru Rosianu \" -p #{output} -v #{manifest.version} ." 88 | shelljs.cd '../..' 89 | 90 | # Make packages for all platforms 91 | gulp.task 'pack:all', (callback) -> 92 | runSequence 'pack:osx64', 'pack:win32', 'pack:linux32:deb', 'pack:linux64:deb', callback 93 | 94 | # Build osx64 and run it 95 | gulp.task 'run:osx64', ['build:osx64'], -> 96 | shelljs.exec 'open ./build/Starter/osx64/Starter.app' 97 | 98 | # Run osx64 without building 99 | gulp.task 'open:osx64', -> 100 | shelljs.exec 'open ./build/Starter/osx64/Starter.app' 101 | 102 | # Upload release to GitHub 103 | gulp.task 'release', ['pack:all'], (callback) -> 104 | gulp.src './dist/*' 105 | .pipe $.githubRelease 106 | draft: true 107 | manifest: manifest 108 | 109 | # Make packages for all platforms by default 110 | gulp.task 'default', ['pack:all'] 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter", 3 | "version": "0.1.0", 4 | "description": "NW.js starter project powered by Gulp.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:Aluxian/nwjs-starter.git" 8 | }, 9 | "scripts": { 10 | "postinstall": "(cd src && npm install)" 11 | }, 12 | "devDependencies": { 13 | "coffee-script": "^1.9.3", 14 | "gulp": "^3.8.11", 15 | "gulp-github-release": "^1.0.3", 16 | "gulp-load-plugins": "^0.10.0", 17 | "gulp-node-webkit-builder": "^1.1.1", 18 | "gulp-uglify": "^1.2.0", 19 | "merge-stream": "^0.1.7", 20 | "run-sequence": "^1.1.0", 21 | "shelljs": "^0.5.0" 22 | }, 23 | "optionalDependencies": { 24 | "gulp-appdmg": "^1.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/screenshot.jpg -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | border: none; 5 | } 6 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Starter 5 | 6 | 7 | 8 |

Hello World!

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | var gui = require('nw.gui'); 2 | var win = gui.Window.get(); 3 | 4 | var platform = require('./components/platform'); 5 | var updater = require('./components/updater'); 6 | var menus = require('./components/menus'); 7 | var settings = require('./components/settings'); 8 | var windowBehaviour = require('./components/window-behaviour'); 9 | var dispatcher = require('./components/dispatcher'); 10 | 11 | // Ensure there's an app shortcut for toast notifications to work on Windows 12 | if (platform.isWindows) { 13 | gui.App.createShortcut(process.env.APPDATA + "\\Microsoft\\Windows\\Start Menu\\Programs\\Starter.lnk"); 14 | } 15 | 16 | // Add dispatcher events 17 | dispatcher.addEventListener('win.alert', function(data) { 18 | data.win.window.alert(data.message); 19 | }); 20 | 21 | dispatcher.addEventListener('win.confirm', function(data) { 22 | data.callback(data.win.window.confirm(data.message)); 23 | }); 24 | 25 | // Window state 26 | windowBehaviour.restoreWindowState(win); 27 | windowBehaviour.bindWindowStateEvents(win); 28 | 29 | // Check for update 30 | if (settings.checkUpdateOnLaunch) { 31 | updater.checkAndPrompt(gui.App.manifest, win); 32 | } 33 | 34 | // Run as menu bar app 35 | if (settings.asMenuBarAppOSX) { 36 | win.setShowInTaskbar(false); 37 | menus.loadTrayIcon(win); 38 | } 39 | 40 | // Load the app menus 41 | menus.loadMenuBar(win) 42 | if (platform.isWindows) { 43 | menus.loadTrayIcon(win); 44 | } 45 | 46 | // Adjust the default behaviour of the main window 47 | windowBehaviour.set(win); 48 | windowBehaviour.setNewWinPolicy(win); 49 | 50 | // Add a context menu 51 | menus.injectContextMenu(win, window, document); 52 | 53 | // Reload the app periodically until it loads 54 | var reloadIntervalId = setInterval(function() { 55 | if (win.window.navigator.onLine) { 56 | clearInterval(reloadIntervalId); 57 | } else { 58 | win.reload(); 59 | } 60 | }, 10 * 1000); 61 | -------------------------------------------------------------------------------- /src/components/dispatcher.js: -------------------------------------------------------------------------------- 1 | var callbacks = {}; 2 | var queue = []; 3 | 4 | setInterval(function() { 5 | while (queue.length) { 6 | var event = queue[0][0]; 7 | var data = queue[0][1]; 8 | 9 | callbacks[event].forEach(function(callback) { 10 | callback(data); 11 | }); 12 | 13 | queue.splice(0, 1); 14 | } 15 | }, 100); 16 | 17 | /** 18 | * Like an EventEmitter, but runs differently. Workaround for dialog crashes. Singleton. 19 | */ 20 | module.exports = { 21 | addEventListener: function(event, callback) { 22 | if (!callbacks[event]) { 23 | callbacks[event] = []; 24 | } 25 | 26 | callbacks[event].push(callback); 27 | }, 28 | 29 | removeAllListeners: function(event) { 30 | callbacks[event] = []; 31 | }, 32 | 33 | trigger: function(event, data) { 34 | queue.push([event, data]); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/menus.js: -------------------------------------------------------------------------------- 1 | var gui = window.require('nw.gui'); 2 | var clipboard = gui.Clipboard.get(); 3 | var AutoLaunch = require('auto-launch'); 4 | var windowBehaviour = require('./window-behaviour'); 5 | var dispatcher = require('./dispatcher'); 6 | var platform = require('./platform'); 7 | var settings = require('./settings'); 8 | var updater = require('./updater'); 9 | 10 | module.exports = { 11 | /** 12 | * The main settings items. Their placement differs for each platform: 13 | * - on OS X they're in the top menu bar 14 | * - on Windows they're in the tray icon's menu 15 | * - on all 3 platform, they're also in the right-click context menu 16 | */ 17 | settingsItems: function(win, keep) { 18 | var self = this; 19 | return [{ 20 | label: 'Reload', 21 | click: function() { 22 | windowBehaviour.saveWindowState(win); 23 | win.reload(); 24 | } 25 | }, { 26 | type: 'checkbox', 27 | label: 'Open Links in the Browser', 28 | setting: 'openLinksInBrowser', 29 | click: function() { 30 | settings.openLinksInBrowser = this.checked; 31 | windowBehaviour.setNewWinPolicy(win); 32 | } 33 | }, { 34 | type: 'separator' 35 | }, { 36 | type: 'checkbox', 37 | label: 'Run as Menu Bar App', 38 | setting: 'asMenuBarAppOSX', 39 | platforms: ['osx'], 40 | click: function() { 41 | settings.asMenuBarAppOSX = this.checked; 42 | win.setShowInTaskbar(!this.checked); 43 | 44 | if (this.checked) { 45 | self.loadTrayIcon(win); 46 | } else if (win.tray) { 47 | win.tray.remove(); 48 | win.tray = null; 49 | } 50 | } 51 | }, { 52 | type: 'checkbox', 53 | label: 'Launch on Startup', 54 | setting: 'launchOnStartup', 55 | platforms: ['osx', 'win'], 56 | click: function() { 57 | settings.launchOnStartup = this.checked; 58 | 59 | var launcher = new AutoLaunch({ 60 | name: 'Starter', 61 | isHidden: true // hidden on launch - only works on a mac atm 62 | }); 63 | 64 | launcher.isEnabled(function(enabled) { 65 | if (settings.launchOnStartup && !enabled) { 66 | launcher.enable(function(error) { 67 | if (error) { 68 | console.error(error); 69 | } 70 | }); 71 | } 72 | 73 | if (!settings.launchOnStartup && enabled) { 74 | launcher.disable(function(error) { 75 | if (error) { 76 | console.error(error); 77 | } 78 | }); 79 | } 80 | }); 81 | } 82 | }, { 83 | type: 'checkbox', 84 | label: 'Check for Update on Launch', 85 | setting: 'checkUpdateOnLaunch' 86 | }, { 87 | type: 'separator' 88 | }, { 89 | label: 'Check for Update', 90 | click: function() { 91 | updater.check(gui.App.manifest, function(error, newVersionExists, newManifest) { 92 | if (error || newVersionExists) { 93 | updater.prompt(win, false, error, newVersionExists, newManifest); 94 | } else { 95 | dispatcher.trigger('win.alert', { 96 | win: win, 97 | message: 'You’re using the latest version: ' + gui.App.manifest.version 98 | }); 99 | } 100 | }); 101 | } 102 | }, { 103 | label: 'Launch Dev Tools', 104 | click: function() { 105 | win.showDevTools(); 106 | } 107 | }].map(function(item) { 108 | // If the item has a 'setting' property, use some predefined values 109 | if (item.setting) { 110 | if (!item.hasOwnProperty('checked')) { 111 | item.checked = settings[item.setting]; 112 | } 113 | 114 | if (!item.hasOwnProperty('click')) { 115 | item.click = function() { 116 | settings[item.setting] = item.checked; 117 | }; 118 | } 119 | } 120 | 121 | return item; 122 | }).filter(function(item) { 123 | // Remove the item if the current platform is not supported 124 | return !Array.isArray(item.platforms) || (item.platforms.indexOf(platform.type) != -1); 125 | }).map(function(item) { 126 | var menuItem = new gui.MenuItem(item); 127 | menuItem.setting = item.setting; 128 | return menuItem; 129 | }); 130 | }, 131 | 132 | /** 133 | * Create the menu bar for the given window, only on OS X. 134 | */ 135 | loadMenuBar: function(win) { 136 | if (!platform.isOSX) { 137 | return; 138 | } 139 | 140 | var menu = new gui.Menu({ 141 | type: 'menubar' 142 | }); 143 | 144 | menu.createMacBuiltin('Starter'); 145 | var submenu = menu.items[0].submenu; 146 | 147 | submenu.insert(new gui.MenuItem({ 148 | type: 'separator' 149 | }), 1); 150 | 151 | // Add the main settings 152 | this.settingsItems(win, true).forEach(function(item, index) { 153 | submenu.insert(item, index + 2); 154 | }); 155 | 156 | // Watch the items that have a 'setting' property 157 | submenu.items.forEach(function(item) { 158 | if (item.setting) { 159 | settings.watch(item.setting, function(value) { 160 | item.checked = value; 161 | }); 162 | } 163 | }); 164 | 165 | win.menu = menu; 166 | }, 167 | 168 | /** 169 | * Create the menu for the tray icon. 170 | */ 171 | createTrayMenu: function(win) { 172 | var menu = new gui.Menu(); 173 | 174 | // Add the main settings 175 | this.settingsItems(win, true).forEach(function(item) { 176 | menu.append(item); 177 | }); 178 | 179 | menu.append(new gui.MenuItem({ 180 | type: 'separator' 181 | })); 182 | 183 | menu.append(new gui.MenuItem({ 184 | label: 'Show Starter', 185 | click: function() { 186 | win.show(); 187 | } 188 | })); 189 | 190 | menu.append(new gui.MenuItem({ 191 | label: 'Quit Starter', 192 | click: function() { 193 | win.close(true); 194 | } 195 | })); 196 | 197 | // Watch the items that have a 'setting' property 198 | menu.items.forEach(function(item) { 199 | if (item.setting) { 200 | settings.watch(item.setting, function(value) { 201 | item.checked = value; 202 | }); 203 | } 204 | }); 205 | 206 | return menu; 207 | }, 208 | 209 | /** 210 | * Create the tray icon. 211 | */ 212 | loadTrayIcon: function(win) { 213 | if (win.tray) { 214 | win.tray.remove(); 215 | win.tray = null; 216 | } 217 | 218 | var tray = new gui.Tray({ 219 | icon: 'images/icon_' + (platform.isOSX ? 'menubar.tiff' : 'tray.png') 220 | }); 221 | 222 | tray.on('click', function() { 223 | win.show(); 224 | }); 225 | 226 | tray.tooltip = 'Starter'; 227 | tray.menu = this.createTrayMenu(win); 228 | 229 | // keep the object in memory 230 | win.tray = tray; 231 | }, 232 | 233 | /** 234 | * Create a context menu for the window and document. 235 | */ 236 | createContextMenu: function(win, window, document, targetElement) { 237 | var menu = new gui.Menu(); 238 | 239 | if (targetElement.tagName.toLowerCase() == 'input') { 240 | menu.append(new gui.MenuItem({ 241 | label: "Cut", 242 | click: function() { 243 | clipboard.set(targetElement.value); 244 | targetElement.value = ''; 245 | } 246 | })); 247 | 248 | menu.append(new gui.MenuItem({ 249 | label: "Copy", 250 | click: function() { 251 | clipboard.set(targetElement.value); 252 | } 253 | })); 254 | 255 | menu.append(new gui.MenuItem({ 256 | label: "Paste", 257 | click: function() { 258 | targetElement.value = clipboard.get(); 259 | } 260 | })); 261 | } else if (targetElement.tagName.toLowerCase() == 'a') { 262 | menu.append(new gui.MenuItem({ 263 | label: "Copy Link", 264 | click: function() { 265 | clipboard.set(targetElement.href); 266 | } 267 | })); 268 | } else { 269 | var selection = window.getSelection().toString(); 270 | if (selection.length > 0) { 271 | menu.append(new gui.MenuItem({ 272 | label: "Copy", 273 | click: function() { 274 | clipboard.set(selection); 275 | } 276 | })); 277 | } 278 | } 279 | 280 | this.settingsItems(win, false).forEach(function(item) { 281 | menu.append(item); 282 | }); 283 | 284 | return menu; 285 | }, 286 | 287 | /** 288 | * Listen for right clicks and show a context menu. 289 | */ 290 | injectContextMenu: function(win, window, document) { 291 | document.body.addEventListener('contextmenu', function(event) { 292 | event.preventDefault(); 293 | this.createContextMenu(win, window, document, event.target).popup(event.x, event.y); 294 | return false; 295 | }.bind(this)); 296 | } 297 | }; 298 | -------------------------------------------------------------------------------- /src/components/platform.js: -------------------------------------------------------------------------------- 1 | var platform = process.platform; 2 | var arch = process.arch === 'ia32' ? '32' : '64'; 3 | 4 | platform = platform.indexOf('win') === 0 ? 'win' 5 | : platform.indexOf('darwin') === 0 ? 'osx' 6 | : 'linux'; 7 | 8 | module.exports = { 9 | isOSX: platform === 'osx', 10 | isWindows: platform === 'win', 11 | isLinux: platform === 'linux', 12 | name: platform + arch, 13 | type: platform, 14 | arch: arch 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/settings.js: -------------------------------------------------------------------------------- 1 | var Store = require('jfs'); 2 | var path = require('path'); 3 | var gui = window.require('nw.gui'); 4 | 5 | var DEFAULT_SETTINGS = { 6 | launchOnStartup: false, 7 | checkUpdateOnLaunch: false, 8 | openLinksInBrowser: true, 9 | asMenuBarAppOSX: false, 10 | windowState: {}, 11 | }; 12 | 13 | var db = new Store(path.join(gui.App.dataPath, 'preferences.json')); 14 | var settings = db.getSync('settings'); 15 | var watchers = {}; 16 | 17 | // Watch changes to the storage 18 | settings.watch = function(name, callback) { 19 | if (!Array.isArray(watchers[name])) { 20 | watchers[name] = []; 21 | } 22 | 23 | watchers[name].push(callback); 24 | }; 25 | 26 | // Save settings every time a change is made and notify watchers 27 | Object.observe(settings, function(changes) { 28 | db.save('settings', settings, function(err) { 29 | if (err) { 30 | console.error('Could not save settings', err); 31 | } 32 | }); 33 | 34 | changes.forEach(function(change) { 35 | var newValue = change.object[change.name]; 36 | var keyWatchers = watchers[change.name]; 37 | 38 | // Call all the watcher functions for the changed key 39 | if (keyWatchers && keyWatchers.length) { 40 | for (var i = 0; i < keyWatchers.length; i++) { 41 | try { 42 | keyWatchers[i](newValue); 43 | } catch(ex) { 44 | console.error(ex); 45 | keyWatchers.splice(i--, 1); 46 | } 47 | } 48 | } 49 | }); 50 | }); 51 | 52 | // Ensure the default values exist 53 | Object.keys(DEFAULT_SETTINGS).forEach(function(key) { 54 | if (!settings.hasOwnProperty(key)) { 55 | settings[key] = DEFAULT_SETTINGS[key]; 56 | } 57 | }); 58 | 59 | module.exports = settings; 60 | -------------------------------------------------------------------------------- /src/components/updater.js: -------------------------------------------------------------------------------- 1 | var gui = window.require('nw.gui'); 2 | var platform = require('./platform'); 3 | var dispatcher = require('./dispatcher'); 4 | var request = require('request'); 5 | var semver = require('semver'); 6 | 7 | module.exports = { 8 | /** 9 | * Check if there's a new version available. 10 | */ 11 | check: function(manifest, callback) { 12 | request(manifest.manifestUrl, function(error, response, body) { 13 | if (error) { 14 | return callback(error); 15 | } 16 | 17 | var newManifest = JSON.parse(body); 18 | var newVersionExists = semver.gt(newManifest.version, manifest.version); 19 | 20 | callback(null, newVersionExists, newManifest); 21 | }); 22 | }, 23 | 24 | /** 25 | * Show a dialog to ask the user to update. 26 | */ 27 | prompt: function(win, ignoreError, error, newVersionExists, newManifest) { 28 | if (error) { 29 | if (!ignoreError) { 30 | dispatcher.trigger('win.alert', { 31 | win: win, 32 | message: 'Error while trying to update: ' + error 33 | }); 34 | } 35 | 36 | return; 37 | } 38 | 39 | if (newVersionExists) { 40 | var updateMessage = 'There’s a new version available (' + newManifest.version + '). Would you like to download the update now?'; 41 | 42 | dispatcher.trigger('win.confirm', { 43 | win: win, 44 | message: updateMessage, 45 | callback: function(result) { 46 | if (result) { 47 | gui.Shell.openExternal(newManifest.packages[platform.name]); 48 | gui.App.quit(); 49 | } 50 | } 51 | }); 52 | } 53 | }, 54 | 55 | /** 56 | * Check for update and ask the user to update. 57 | */ 58 | checkAndPrompt: function(manifest, win) { 59 | this.check(manifest, this.prompt.bind(this, win, true)); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/window-behaviour.js: -------------------------------------------------------------------------------- 1 | var gui = window.require('nw.gui'); 2 | var platform = require('./platform'); 3 | var settings = require('./settings'); 4 | 5 | module.exports = { 6 | /** 7 | * Update the behaviour of the given window object. 8 | */ 9 | set: function(win) { 10 | // Show the window when the dock icon is pressed 11 | gui.App.removeAllListeners('reopen'); 12 | gui.App.on('reopen', function() { 13 | win.show(); 14 | }); 15 | 16 | // Don't quit the app when the window is closed 17 | if (!platform.isLinux) { 18 | win.removeAllListeners('close'); 19 | win.on('close', function(quit) { 20 | if (quit) { 21 | this.saveWindowState(win); 22 | win.close(true); 23 | } else { 24 | win.hide(); 25 | } 26 | }.bind(this)); 27 | } 28 | }, 29 | 30 | /** 31 | * Change the new window policy to open links in the browser or another window. 32 | */ 33 | setNewWinPolicy: function(win) { 34 | win.removeAllListeners('new-win-policy'); 35 | win.on('new-win-policy', function(frame, url, policy) { 36 | if (settings.openLinksInBrowser) { 37 | gui.Shell.openExternal(url); 38 | policy.ignore(); 39 | } else { 40 | policy.forceNewWindow(); 41 | } 42 | }); 43 | }, 44 | 45 | /** 46 | * Listen for window state events. 47 | */ 48 | bindWindowStateEvents: function(win) { 49 | win.removeAllListeners('maximize'); 50 | win.on('maximize', function() { 51 | win.sizeMode = 'maximized'; 52 | }); 53 | 54 | win.removeAllListeners('unmaximize'); 55 | win.on('unmaximize', function() { 56 | win.sizeMode = 'normal'; 57 | }); 58 | 59 | win.removeAllListeners('minimize'); 60 | win.on('minimize', function() { 61 | win.sizeMode = 'minimized'; 62 | }); 63 | 64 | win.removeAllListeners('restore'); 65 | win.on('restore', function() { 66 | win.sizeMode = 'normal'; 67 | }); 68 | }, 69 | 70 | /** 71 | * Store the window state. 72 | */ 73 | saveWindowState: function(win) { 74 | var state = { 75 | mode: win.sizeMode || 'normal' 76 | }; 77 | 78 | if (state.mode == 'normal') { 79 | state.x = win.x; 80 | state.y = win.y; 81 | state.width = win.width; 82 | state.height = win.height; 83 | } 84 | 85 | settings.windowState = state; 86 | }, 87 | 88 | /** 89 | * Restore the window size and position. 90 | */ 91 | restoreWindowState: function(win) { 92 | var state = settings.windowState; 93 | 94 | if (state.mode == 'maximized') { 95 | win.maximize(); 96 | } else { 97 | win.resizeTo(state.width, state.height); 98 | win.moveTo(state.x, state.y); 99 | } 100 | 101 | win.show(); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/src/images/icon.png -------------------------------------------------------------------------------- /src/images/icon_menubar.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/src/images/icon_menubar.tiff -------------------------------------------------------------------------------- /src/images/icon_menubar_alert.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/src/images/icon_menubar_alert.tiff -------------------------------------------------------------------------------- /src/images/icon_tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/src/images/icon_tray.png -------------------------------------------------------------------------------- /src/images/icon_tray_alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aluxian/nwjs-starter/1eefb7bf0ad89091143644c849314c94771a2da4/src/images/icon_tray_alert.png -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "app.html", 3 | "name": "Starter", 4 | "version": "0.1.0", 5 | "app-id": "com.aluxian.starter", 6 | "chromium-args": "--disable-setuid-sandbox", 7 | "window": { 8 | "width": 800, 9 | "height": 600, 10 | "title": "Starter", 11 | "icon": "icons/icon.png", 12 | "toolbar": false, 13 | "show": false 14 | }, 15 | "webkit": { 16 | "plugin": true 17 | }, 18 | "dependencies": { 19 | "async": "^1.0.0", 20 | "auto-launch": "^0.1.18", 21 | "jfs": "^0.2.5", 22 | "request": "^2.56.0", 23 | "semver": "^4.3.5" 24 | }, 25 | "manifestUrl": "https://raw.githubusercontent.com/Aluxian/nwjs-starter/master/src/package.json", 26 | "packages": { 27 | "osx64": "https://github.com/Aluxian/nwjs-starter/releases/download/v0.1.0/Starter.dmg", 28 | "win32": "https://github.com/Aluxian/nwjs-starter/releases/download/v0.1.0/StarterSetup.exe", 29 | "linux32": "https://github.com/Aluxian/nwjs-starter/releases/download/v0.1.0/Starter_linux32.deb", 30 | "linux64": "https://github.com/Aluxian/nwjs-starter/releases/download/v0.1.0/Starter_linux64.deb" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Use this file to quickly change the app version. 4 | # It will also tag, commit the change and push it. 5 | # 6 | # Usage: ./version.sh 1.2.0 7 | 8 | # Check $1 9 | if [ -z "$1" ] 10 | then 11 | echo "Version is required." 12 | fi 13 | 14 | # Replace version in package.json files 15 | sed -i.bak "s/\"version\": \".*\"/\"version\": \"$1\"/g" ./package.json 16 | sed -i.bak "s/\"version\": \".*\"/\"version\": \"$1\"/g" ./src/package.json 17 | sed -i.bak "s/download\/v.*\/UnofficialWhatsApp/download\/v$1\/UnofficialWhatsApp/g" ./src/package.json 18 | 19 | # Clean up 20 | rm ./package.json.bak 21 | rm ./src/package.json.bak 22 | 23 | # Edit CHANGELOG 24 | vim ./CHANGELOG 25 | 26 | # Git commit 27 | git add . 28 | git commit -m "New version v$1" 29 | git tag -a "v$1" -m "v$1" 30 | 31 | # TODO Paste all commits since the last tag into CHANGELOG 32 | --------------------------------------------------------------------------------