📜 ⬆️ ⬇️

Desktop JavaScript applications. Part 2

This article is a continuation of the article "Desktop applications in JavaScript. Part 1 " . In the previous section, we looked at the following:

As part of this article, we will look at creating a password storage application. The application is relatively simple and is mostly a prototype for real. However, if you want and have time, you can modify it and use it for everyday work.


The basis of the application for storing passwords


As you know, the development can be conducted both in pure JavaScript, and using a variety of frameworks, of which there is such a huge amount that sometimes you lose yourself in their diversity and for a long time you cannot decide what to choose . For application development, patterns that start with MV ( MVC , MVVM , MVP ) are especially popular. One of the frameworks using a similar pattern is Angular JS , which is what we will use when developing our application. If you are not familiar with it, I advise you to read the documentation ( tutorial , API ), you can also learn basic information in the manual in Russian.

What will be the application? All data is displayed in the form of a table, with the login should be visible, and instead of the password should be asterisks. The user can add a new login / password, as well as delete entries that have become unnecessary. In addition, it is necessary to provide for the possibility of editing.
')
We implement the basic functionality of the application. To do this, you need to create a folder in which we will locate the source code, and also place package.json in it (on how to do this, see Part 1).

Create a basic folder structure consisting of the following directories:

In addition, we will add the index.html file to the project root, which will be the entry point to the application. Create the basic markup:
Basic markup
<html ng-app="main"> <head> <meta charset="utf-8"> <title>Password keeper</title> <link rel="stylesheet" type="text/css" href="css/index.css"> </head> <body> <table> <thead> <tr> <td></td> <td>Login</td> <td>Password</td> <td></td> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> <td><a></a></td> </tr> </tbody> <tfoot> <tr> <td></td> <td><a></a></td> <td></td> <td></td> </tr> </tfoot> </table> <script type="text/javascript" src="lib/angular.min.js"></script> </body> </html> 



Since the application is fairly simple, we will create a controller and within it we will place all the main application logic (as the logic grows, you need to add the Service folder and place services in it, where all the complex logic should be placed, the controllers should be left "Thin"). Let's call the main controller, and the main.ctrl.js controller file . So, the blank for the controller:

 (function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', [MainCtrl]); function MainCtrl() { this.data = []; return this; } })(); 


Data containing logins / passwords for our prototype will be placed in the data array. To simplify the implementation of editing, create your own EditableText element and arrange it as a directive. This element will work as follows: the element is displayed as text, when you click on an element, the text turns into a textual input field, and when the focus is lost, the element is again displayed as text. To do this, we will create a markup file for the editableText.html directive inside the View folder:

 <input ng-model="value"> <span ng-click="edit()">{{value}}</span> 


And inside the directive folder, create an editableText.js file:
Editable-text directive code
 (function () { 'use strict'; angular .module('main') .directive('editableText', [editableText]); function editableText() { var directive = { restrict: 'E', scope: { value: "=" }, templateUrl: 'view/editableText.html', link: function ( $scope, element, attrs ) { //     input   var inputElement = angular.element( element.children()[0] ); element.addClass( 'editable-text' ); // ,     ,    //     $scope.edit = function () { element.addClass( 'active' ); inputElement[0].focus(); }; //   , ..     inputElement.prop( 'onblur', function() { element.removeClass( 'active' ); }); } }; return directive; } })(); 



The directive also requires some styles that can be placed inside index.css :

 .editable-text span { cursor: pointer; } .editable-text input { display: none; } .editable-text.active span { display: none; } .editable-text.active input { display: inline-block; } 


The use of the directive is as follows:

 <editable-text value="variable"></editable-text> 


For the login, everything is in order - we display either the text or the text field, but what about the password, because we should not show it. Add a crypto field to the scope of our directive as follows:

 scope: { value: "=", crypto: "=" } 


And also change the markup directive:

 <input ng-model="value"> <span ng-click="edit()">{{crypto?'***':value}}</span> 


In addition, you must not forget to add scripts to index.html:

 <script type="text/javascript" src="lib/angular.min.js"></script> <script type="text/javascript" src="controller/main.ctrl.js"></script> <script type="text/javascript" src="directive/editableText.js"></script> 


It's time to add functionality. Change the controller as follows:
Controller code
 function MainCtrl() { var self = this; this.data = []; this.remove = remove; this.copy = copy; this.add = add; return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ //   }; function add(){ self.data.push({login: "login", password: "password"}); }; } 



In addition, changes to the markup are needed:
Summary markup
 <body ng-controller="MainCtrl as ctrl"> <table> <thead> <tr> <td></td> <td>Login</td> <td>Password</td> <td></td> </tr> </thead> <tbody> <tr ng-repeat="record in ctrl.data track by $index"> <td><a ng-click="ctrl.copy($index)">{{$index}}</a></td> <td><editable-text value="record.login"></editable-text></td> <td><editable-text value="record.password" crypto="true"></editable-text></td> <td><a ng-click="ctrl.remove($index)"></a></td> </tr> </tbody> <tfoot> <tr> <td></td> <td><a ng-click="ctrl.add()"></a></td> <td></td> <td></td> </tr> </tfoot> </table> <script type="text/javascript" src="lib/angular.min.js"></script> <script type="text/javascript" src="controller/main.ctrl.js"></script> <script type="text/javascript" src="directive/editableText.js"></script> </body> 



At this stage, you can do styling. An example of simple styling (I remind you that we add styles to index.css , however, if there are quite a lot of styles, you can split the styles into files or even use a preprocessor, for example LESS ):
An example of styling an application
 table { border-collapse: collapse; margin: auto; width: calc(100% - 40px); } table, table thead, table tfoot, table tbody tr td:first-child, table tbody tr td:nth-child(2), table tbody tr td:nth-child(3), table thead tr td:nth-child(2), table thead tr td:nth-child(3) { border: 1px solid #000; } table td { padding: 5px; } table thead { background: #EEE; } table tbody tr td:first-child { background: #CCC; } table tbody tr td:nth-child(2) { background: #777; color: #FFF; } table tbody tr td:nth-child(3) { background: #555; color: #FFF; } table thead tr td:nth-child(2),table thead tr td:nth-child(3) { text-align: center; } table a { font-size: smaller; cursor: pointer; } 



The application is as follows:



Work with clipboard


So, the basis of the application is ready, but it does not yet realize the main purpose; we cannot copy passwords (or rather, we can, but rather inconveniently). To begin, consider the work with the clipboard in NW.js
There is a special object - Clipboard , which is used as an abstraction for the Windows clipboard and GTK, as well as for pasteboard (Mac). At the time of this writing, support is provided for writing and reading only text.
To work with the object, we need the familiar module nw.gui :

 var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); 


Please note that we can not create your object, we can only get the system. Three methods are supported:

Now you can finish the application functionality, and the controller will look like this:
Controller code
 function MainCtrl() { var self = this; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); this.data = []; this.remove = remove; this.copy = copy; this.add = add; return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; } 



Password storage


After the application was launched, the user stored several passwords, closed the application. The next day it turns out that the passwords are gone. The problem is that we kept them in a normal local variable, which was deleted when closed.
In the third part, we will look at how NW.js works with databases, but for now we will store passwords in localStorage . Before proceeding to the creation of functionality, (although the application we have so far only a prototype) you need to take care of security. To do this, we do not need to store passwords in the clear.
For encryption / decryption there are various libraries in javascript . One such library is crypto-js . Install it as a module for node.js. The library supports a large number of standards, a complete list of which can be found in the documentation. At the same time, you can connect both all modules and a separate module:

 //   ,         CryptoJS.HmacSHA1 var CryptoJS = require("crypto-js"); //   ,  AES var AES = require("crypto-js/aes"); 


To encrypt a message, use the encrypt method:

 var ciphertext = CryptoJS.AES.encrypt('', ' '); 


Decryption is a bit more complicated:

 var bytes = CryptoJS.AES.decrypt(ciphertext.toString(), ' '); var plaintext = bytes.toString(CryptoJS.enc.Utf8); 


Let's modify our application so that we can save passwords when closing the application and load them at startup.
Let's create the crypto.svc service and place it in the service folder (if you have not created this folder yet, create it in the application root):
Service code
 (function () { 'use strict'; angular .module('main') .factory('CryptoService', [CryptoService]); function CryptoService() { var CryptoJS = require("crypto-js"); var secretKey = "secretKey"; var service = { encrypt: encrypt, decrypt: decrypt }; return service; function encrypt(data) { return CryptoJS.AES.encrypt(JSON.stringify(data), secretKey); } function decrypt(text) { var bytes = CryptoJS.AES.decrypt(text.toString(), secretKey); var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); return decryptedData; } } })(); 



To use our service, we upgrade the controller:
Controller code
 (function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]); function MainCtrl($scope, CryptoService) { var self = this; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); var localStorageKey = "loginPasswordData" this.data = []; this.remove = remove; this.copy = copy; this.add = add; load(); $scope.$watch("ctrl.data", save, true); return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; function load(){ var text = localStorage.getItem(localStorageKey); if(text) { self.data = CryptoService.decrypt(text); } } function save(){ if(self.data) { localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data)); } } } })(); 



In addition to connecting the service, we also needed the $ scope service that already exists in AngularJS . We use the $ watch method to track the moment of data change in order to save it in time (note that we pass true as the third argument so that changes not only in the array, i.e. insert / delete, but also changes) are tracked elements of the array, ie, changing the login or password of a separate element of the array). Data loading occurs when opening the view.

Minimize to tray


The basis of the application is ready, but as you know, such programs are often minimized to the system tray in order not to overload the user with an abundance of open windows.
Another abstraction introduced in NW.js is tray: System Tray Icon for Windows, Status Icon for GTK, Status Item for OSX. This object is created using the gui module known to us:

 var gui = require("nw.gui"); var tray = new gui.Tray({ title: 'Tray', icon: 'img/icon.png' }); 

When working with this object, you need to take care of the scope of the variable; if created inside a function, it will soon be removed by the GC . When creating an object, you can immediately create properties, as we did in the example, or you can take care of this a little later. The following properties can be defined for this object:

In order to use the tray in our application, you need to create any markup element, the most obvious option is the button . Next, you need to subscribe to the click event, and use the methods of the window object, which we will now become familiar with.

We work with a window


To work with a window, you must either get an existing window or create a new one. So, in order to get the current window, in which our application is displayed, you need to run the command:

 var win = gui.Window.get(); 

And to create a new window, you must specify the address where the page is located to open in this window, as well as the parameters to open (these parameters correspond to what we specify when creating the manifest, see the first part of the series of articles):

 var win = gui.Window.open ('https://myurl', { position: 'center', width: 901, height: 127 }); 

Also in the parameters you can pass a special property focus: true , if you specify which, the newly created window will immediately receive focus, otherwise the focus will remain on our current window.
If we create a new window and want to do something with it after it is created, you need to subscribe to the appropriate event:

 win.on ('loaded', function(){ //   document  ,       var document = win.window.document; //      ... }); 


As you can see from the example, one of the window properties is the window object, from which we can get the rest of the elements, including the document. In addition to this property, the window also supports many others:
These properties can not only read, but also change. In addition to them, there are also read-only properties (they are all logical and can be true or false)

In addition to properties, the object supports a large number of methods. The main methods are given below and are grouped for convenience.
Methods for changing the position and size of the window:

Methods for working with focus and visibility:

Methods for managing the coagulation / deployment, closing the window:

Methods for state management:

Methods for controlling the ability to resize a window

So, having got acquainted with the objects tray and window , we write the functionality of minimizing to tray. To do this, it is necessary (as mentioned above) to add an element to the markup, for example, a button or a link:
 <a ng-click="ctrl.toTray()"> </a> 

And change the controller as follows:
Controller code
 (function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]); function MainCtrl($scope, CryptoService) { var self = this; var localStorageKey = "loginPasswordData" this.data = []; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); var win = gui.Window.get(); var tray = new gui.Tray({ title: 'Tray', icon: 'img/test.png' }); tray.on("click", restoreFromTray); this.remove = remove; this.copy = copy; this.add = add; this.toTray = toTray; load(); $scope.$watch("ctrl.data", save, true); return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; function load(){ var text = localStorage.getItem(localStorageKey); if(text) { self.data = CryptoService.decrypt(text); } } function save(){ if(self.data) { localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data)); } } function toTray(){ win.minimize(); win.setShowInTaskbar(false); } function restoreFromTray(){ win.restore(); win.setShowInTaskbar(true); } } })(); 


Also, for this example to work, you must create an img folder and place the tray icon there (in this example, img / test.png ).

Conclusion


Within the framework of this article, we wrote a prototype of the application, which you can improve in various ways: starting from styles and ending with improvements in functionality. For example:
Successes in programming!

Source: https://habr.com/ru/post/275615/


All Articles