No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
The Jared Wilcurt 6ae4b3b7a4 Update README.md 2 months ago
README.md Update README.md 2 months ago
screenshot.png Upload screenshot 1 year ago

README.md

screenshot of finished app running

Introduction

This repo contains step-by-step instructions for creating your first desktop app. It was created for a live workshop I gave. It is split into 3 sections with a TLDR style summary at the end of each section.

If you’d like to see the finished project checkout the finished-app branch.

In this workshop you will learn the basics of:

  • Node.js
  • NPM
  • NW.js
  • Vue.js
  • Packaging a desktop app for distribution

Prerequisites for this workshop:

  • A laptop (any of these should work)
    • A Windows laptop (32-Bit or 64-Bit) with admin rights.
    • An Apple laptop (64-Bit) with access to “sudo” (though we probably won’t need it)
    • A laptop with Ubuntu, Debian, Zorin, or other Debian-based version of Linux (32-Bit or 64-Bit) with access to “sudo”.
    • Note: If you are using a laptop with an ARM processor (like a Chromebook), you will not be able to participate, but you can still watch and take notes.
  • A Text Editor like Sublime Text, VS Code, Brackets, or Atom.
  • Basic knowledge of HTML/CSS/JS
  • Basic usage of the command prompt/terminal
  • Install Node.js before you arrive unless you need help installing it.

Setting up the project

Note: Whenever this tutorial says Run that means you should run the command in the command line or Terminal.

  1. Install Node and NPM
    • http://nodejs.org
    • NPM comes with Node on Windows and OSX.
    • If you are on linux, you will need to google how to install NPM as instructions vary for different distros.
  2. Run node -v and npm -v to ensure they are installed
  3. If you have a folder where you keep all your projects, create a new folder in there called “battery”, otherwise just make it on your desktop.
    • From this point on, assume all files referenced will be inside this “battery” folder
  4. Navigate to the “battery” folder in the command line/terminal
  5. Run npm init
    • Keep the defaults for everything except the entry point, set that to index.html
  6. Run npm install --save-dev nw@sdk
  7. Edit the package.json file in your text editor. In the scripts section. Add this script:
    • "start": "nw ."
  8. Create an index.html file in your “battery” folder
  9. Edit the index.html file and put this inside it:
<!DOCTYPE html>
<html>
  <head>
    <title>Hello World</title>
  </head>
  <body>
    <h2>Hello World</h2>
  </body>
</html>

10. Run npm start

  • A window with our “Hello World” message should have launched. You can close it now, that means it worked!
  • At any step from now on, you can run npm start to launch the app and see your changes.
  • Once open try one of these commands to open the Developer Tools, some commands only work on some OS’s.
    • F12
    • Fn + F12
    • Ctrl + Shift + I
    • Ctrl + Shift + J
    • Cmd + Shift + I
    • Cmd + Shift + J

11. To stop running your app you can close its window or click on your command prompt/teminal and run one of these commands (depending on OS)

  • Ctrl + C
  • Ctrl + Z

The steps in the above section are the same for basically any desktop app you want to create.

Section Summary:

  1. npm init (set entry to index.html)
  2. npm install --save-dev nw@sdk
  3. Edit the package.json to have "nw ." for the "start" script
  4. Create an empty index.html file
  5. npm start

Building the app

  1. For this project we will be using the Vue.js framework to make instructions simpler and the Node module “SystemInformation” to give use easy access to information about the computer’s battery.
    • Run npm install --save vue systeminformation
  2. Create a scripts folder and a styles folder in your “battery” project folder.
  3. In the scripts folder create a file called main.js
  4. In the styles folder create a file called style.css
  5. Replace the code in your index.html file with this:
<!DOCTYPE html>
<html>
  <head>
    <title>Battery Meter</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="styles/style.css">
  </head>
  <body>
    <h2>Battery Meter</h2>
    <script src="node_modules/vue/dist/vue.min.js"></script>
    <script src="scripts/main.js"></script>
  </body>
</html>

6. Now we want to start adding in some dynamic functionality, so we’re going to create a <div> with an id of “app” under the h2 to put all the dynamic stuff in, so Vue can handle it for us.

<h2>Battery Meter</h2>
<div id="app">
</div>

7. In our scripts\main.js file, lets set up Vue so it knows the basics.

  • The data will will be what we use in the HTML to display the correct content. Updating the values in our JS will also update the values in the DOM for us
const app = new Vue({
    el: '#app',
    data: {
        percent: 0,
        cycles: 0,
        isCharging: false,
        updatedInfo: false
    }
});

8. Download a battery icon image from IconArchive.com.

  • Download a .PNG image, and if you are on Windows, also get the .ico file, if you are on OSX also get the .icns files.
  • Save the images as icon.png, icon.ico, and app.icns so we can reference them later.

9. In the index.html let’s add some content into our <div id="app"> and have it reference those pieces of data from Vue.

  • There’s a lot here, so lets break it up into each line
  • The meter will create a bar on the page for us to set a value for. The :value="percent" will set it to whatever the data.percent value is.
  • We’re going to put in an image and a big text of the current battery percent. The {{ percent }} will be set by whatever data.percent is.
  • The next two blocks have a v-if and v-else. Based on if isCharging is true or false, it will only show one of the two blocks.
  • Not all batteries keep track of a Cycle count. So we only show the amount of cycles if it’s greater than 0.
  • We want to show when the app checked for battery, so we are going to set its class to updated if updatedInfo is true
<div id="app">
    <meter :value="percent" min="0" max="100"></meter>

    <h1><img src="icon.png" alt="Battery Icon"><span class="percent">{{ percent }}%</span></h1>

    <div v-if="isCharging" class="charging">Charging</div>
    <div v-else class="charging">Not charging</div>

    <div v-if="cycles > 0" class="cycle">Cycle count: <strong>{{ cycles }}</strong></div>
    <div :class="{'updated': updatedInfo}">Updated</div>
</div>

10. At this point we have some content in the app, but it isn’t very pretty and it isn’t using real data. So let’s fix that.
11. In your styles\style.css file add in this:

body {
    font-family: tahoma, sans-serif;
    text-align: center;
}
h2 {
    margin: 20px 0px;
    font-size: 40px;
}
meter {
    width: 270px;
    height: 30px;
}
h1 {
    margin: 20px 0px;
    color: #030;
    font-size: 60px;
    font-weight: bold;
}
img,
.percent {
    max-height: 60px;
    margin: 0px 10px;
    vertical-align: middle;
}
.charging {
    color: #797;
    font-style: italic;
}
.charging,
.cycle {
    margin-bottom: 10px;
}
.updated {
    opacity: 0.0;
    transition: 2s ease all;
}

12. That’s all just plain CSS, so not a lot to explain. One item worth noting is that the .updated class has a 2 second animation that will fade it out. This will come in handy later.
13. Now let’s add some live data to the app using the systeminformation Node module.
14. Add this to the bottom of your main.js file.

// Import the systeminformation module and store it in the si variable
const si = require('systeminformation');

// Create a function that can be continuosly called to get updated battery info
function updateData () {
    // Access the battery API of si and pass in a callback function
    si.battery(function (data) {
        // Force the value of updatedInfo in our Vue app to be false,
        // this will show the text "Updated" on the page.
        app.updatedInfo = false;
        // Update the data in our Vue app with the data from si
        app.percent = data.percent;
        app.isCharging = data.ischarging;
        app.cycles = data.cyclecount;
        // To force a CSS animation to occur we wait 1 second
        setTimeout(function () {
            // Then re-add the class of "updated" on the page so the text fades away
            app.updatedInfo = true;
        }, 1000);
    });
}

// Run the function once when the app first loads.
updateData();

// Update the battery data every 10 seconds.
setInterval(updateData, (10 * 1000));

15. Find the "main" section in your package.json add this "window" section right after it:

  "main": "index.html",
  "window": {
    "width": 430,
    "height": 350,
    "icon": "icon.png"
  },

16. Close your app and relaunch it to test the icon and window size.

  • Icon may not show up during development on OSX, but should on Windows and most Ubuntu.

Section summary:

Here’s everything in one index.html file (though you’re better off making seperate .js, .css, .html).

<!DOCTYPE html>
<html>
  <head>
    <title>Battery Meter</title>
    <meta charset="utf-8">
    <style>
      body {
        font-family: tahoma, sans-serif;
        text-align: center;
      }
      h2 {
        margin: 20px 0px;
        font-size: 40px;
      }
      meter {
        width: 270px;
        height: 30px;
      }
      h1 {
        margin: 20px 0px;
        color: #030;
        font-size: 60px;
        font-weight: bold;
      }
      img,
      .percent {
        max-height: 60px;
        margin: 0px 10px;
        vertical-align: middle;
      }
      .charging {
        color: #797;
        font-style: italic;
      }
      .cycle,
      .charging {
        margin-bottom: 10px;
      }
      .updated {
        transition: 2s ease all;
        opacity: 0.0;
      }
    </style>
    <script src="vue.min.js"></script>
  </head>
  <body>
    <h2>Battery Meter</h2>
    <div id="app">
      <meter :value="percent" min="0" max="100"></meter>
      <h1><img src="icon.png" alt="Battery Icon"><span class="percent">{{ percent }}%</span></h1>
      <div v-if="isCharging" class="charging">Charging</div>
      <div v-else class="charging">Not charging</div>
      <div v-if="cycles > 0" class="cycle">Cycle count: <strong>{{ cycles }}</strong></div>
      <div :class="updatedInfo ? 'updated' : ''">Updated</div>
    </div>
    <script>
      var app = new Vue({
        el: '#app',
        data: {
          percent: 0,
          isCharging: false,
          cycles: 0,
          updatedInfo: false
        }
      });
      var si = require('systeminformation');
      function updateData () {
        si.battery(function (data) {
          app.updatedInfo = false;
          app.percent = data.percent;
          app.isCharging = data.ischarging;
          app.cycles = data.cyclecount;
          setTimeout(function () {
            app.updatedInfo = true;
          }, 1000);
        });
      }
      updateData();
      setInterval(updateData, 10000);
    </script>
  </body>
</html>

Also download an image and set it in your package.json like so:

{
  "name": "battery",
  "version": "1.0.0",
  "author": "",
  "description": "",
  "main": "index.html",
  "scripts": {
    "start": "nw ."
  },
  "dependencies": {
    "systeminformation": "^3.23.6",
    "vue": "2.x.x"
  },
  "devDependencies": {
    "nw": "sdk"
  },
  "window": {
    "width": 430,
    "height": 350,
    "icon": "icon.png"
  },
  "license": "MIT"
}

Note: If your version numbers for your dependencies and devDependencies are different, that’s fine.


Packaging your app for distribution

All OS’s:

  1. Create an empty folder called package.nw on your desktop.
    • On OSX call it app.nw instead
  2. Copy all of your project files/folders into it, except the node_modules folder.
  3. Edit the package.nw/package.json file and delete the devDependencies section. Save.
  4. In the commandline cd into package.nw (or app.nw) and run npm install. Now your node_modules folder in the package.nw folder only has your dependencies for your app, and none of the devDependencies.
  5. Next download the normal (not SDK) version of NW.js for your platform:
  6. Open the download and extract the nwjs-v0.xx.x-yyy-zzz folder to your desktop and rename it to battery-1.0.0

Windows/Linux:

  1. Move the package.nw folder inside the battery-1.0.0 folder
  2. Open the battery-1.0.0 folder and rename the nw.exe file to BatteryMeter.exe
    • On Windows you can download Resource Hacker, and open your BatteryMeter.exe in it to change the icon and meta data.
  3. Double-Click BatteryMeter.exe to see your app launch from the folder.
    • Optionally you can try removing files from your battery-1.0.0 folder that come with NW.js and see if your app still runs and doesn’t produce a debug log. Some are only used when you are doing specific tasks, like viewing PDF’s, audio, or video. This can lower your distribution size some (~10MB), but will cause your app to crash if it needs to call upon that file at some point. So if you do this, be sure to test your app thoroughly.
  4. Zip up the battery-1.0.0 folder. Make sure to use a .zip file (not .7z or .rar) since all OS’s can open them natively.
    • You can use 7-Zip with options of deflate and Ultra to get a smaller filesize. (only use deflate, other compression types are not supported on Windows natively)

OSX:

  1. Open the battery-1.0.0 folder on the desktop
  2. Right-click the nwjs.app file and open its contents
  3. Go to the nwjs.app/Contents/Resources folder and move the app.nw folder from your Desktop to here.
  4. Go back to the nwjs.app file and double-click it
    • If you get a security warning, click OK and go to System Preferences > Security & Privacy > Lock icon in corner > Anywhere
  5. Close the app.
  6. In Finder in the nwjs.app/Contents/Resources folder, replace the app.icns and document.icns file with copies of your app.icns icon.
  7. Right-Click nwjs.app > Get Info > Drag your app.icns file to the icon in the top corner of the info window. Close the info window.
  8. Edit the nwjs.app/Contents/Info.plist file in your text editor.
  9. Change these line:
  <key>CFBundleDisplayName</key>
  <string>nwjs</string>

to

  <key>CFBundleDisplayName</key>
  <string>BatteryMeter</string>

and

  <key>CFBundleName</key>
  <string>Chromium</string>

to

  <key>CFBundleName</key>
  <string>BatteryMeter</string>

10. Rename nwjs.app to BatteryMeter.app

11. Navigate to the battery-1.0.0 folder in terminal and run zip -r -9 "battery-1.0.0.zip" "BatteryMeter.app"


Wow, manually packaging your app for distribution is a lot of weird tedius work. Fortunately, there are tools to automate the process, but I wanted you to understand what those tools are doing for you behind the scenes. The most popular option is nw-builder-phoenix.


Additional Resources