Creating an advanced bookmark manager with electron

I created an app for work to manage some of the internal websites, credentials and web apps that I needed to access. Its kinda like a bookmark manager but better ( at least I think so). I built it using electron and since I saw that my electron app tutorial gets a lot of views I thought I would document some of the things I did to create this app. I’m not going to go into to much details but hopefully I will show some of the cool things you can do with electron.

Dependencies

Lets look at some of the dependencies I used to create this app.

In our dev dependencies we have a package we use to build the completed project

How it Works

Well the UI aside basically the workflow is quite simple.

  • Create a new app ( could be a website, folder, notes etc)
  • Enter details of the app
  • Save the new app
  • View all the apps you created

Creating a new app

In our file that holds our menus we have a option called “New App””.

{
 label:"New App",
 click () { mainWindow.webContents.send('create-link'); }
},

If you are confused about mainWindow this is what it may look like

  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

The click function triggers an event called create-link. So lets see what it does.

 electron.ipcRenderer.on('create-link', (event, arg) => {
        clearform();
        $('#popup').modal("show");
        $('#form-state').val("save");
    });

Three things happen. We clear a form. We show a modal. (Clearly we are using bootstrap here). And we set an element value to save. This is pretty straight forward. Whats next we save it to a json database.

Pop up modal – creating a app

In our popup form we have a regular onclick binding for our modal. Nothing fancy here.

$('#save-form').on("click",function(){
     addNewLink();
     return false;
 });

In the addNewLink function we collect the data and save it to a database.

function addNewLink(){
   var title = $('input[name="title"]').val();
   var desc = $('input[name="description"]').val();
  var url = $('input[name="url"]').val();
  var linkObj = {
            "url" : url,
            "icon" : icon,
            "title" : title,
            "description" : desc,
            "color" : iconColor,
            "browser" : browser,
            "comments" : comments
        };
   addLinkToFile(linkObj);
}

To save this to a database we need to look at the addLinkToFile function. This is what saves our data to a json database.

    function addLinkToFile(data ){
        db.get('apps')
            .push({    
                id: shortid.generate(),       
                "url" : data.url,
                "icon" : data.icon,
                'title' : data.title,
                "description" : data.description,
                "color" : data.color,
                "browser" : data.browser,
                "comments" : data.comments
            })
            .write()
            onSaveActions();

    }

Want to see what our database file looks like? Check out this json object below.

   {
      "id": "CH7yZTv8Z",
      "url": "https://www.google.com/",
      "icon": "fa-database",
      "title": "Google Search",
      "description": "A web searching platform",
      "color": "color10",
      "browser": "popup",
      "comments": "<p>Hello world</p>"
    }

User actions

Once the app is loaded the different apps will be displayed in cute little blocks. I used an html template called transit to build it. So each apps looks like the demo page. The company name is placed at the header where the arrow points.

How the ui looks

A user can click anywhere on these little blocks. Lets check out the code for that.

 $(".app-icon").on('click',function(){
     let appUrl = $(this).data("url");
     let browser = $(this).data("browser");
     let appId = $(this).data('id');
     if(browser == "chrome" 
         || browser =="iexplore" 
         || browser=="firefox"){
           openLink(appUrl, browser);
      }
      if(browser == "remote"){
         openCommand(appUrl);
       }
       if(browser == "folder" ){
           openFolder(appUrl);
       }
       if(browser == 'popup' || browser =="info"){
            openAppDetails(appId);
       }
});

The first check is to see if the type of app is a browser. Which calls the function openLink. open is a dependency we added to open browsers or links given a url.

  function openLink( url, browser ){
        open(url, browser);
    }

We can choose the type of browser we want the application to open to. This is great for legacy apps that need probably internet explorer to run.

Screenshot of actions options

If the option is set to remote, the app will try to open windows remote desktop command. The openCommand function deals with this.

  function openCommand( name ){
        exec(`mstsc /v:${name}`, (err, stdout, stderr) => {
            if (err) {
              // node couldn't execute the command
              return;
            }
          });
    }

If the open option is set to folder we use the openFolder function to open a folder. This is great for networking locations or any folder location really.

    function openFolder( name ){
        exec(`start ${name}`, (err, stdout, stderr) => {
            if (err) {
              // node couldn't execute the command
              return;
            }
          });
    }

Finally we have the popup or info option where the user just wants to view stored information. This is great for credentials and passwords. We use the openAppDetails function for this purpose.

    function openAppDetails(id){
        var data = db.get('apps')
        .find({ id: id })
        .value();
        $('#dt-title').text(data.title);
        $('#dt-sub-title').text(data.description);
        $('#dt-action').text(data.url);
        $('#dt-program').text(data.browser);
        $('#dt-comments').html(data.comments);
        $('#dt-icon').attr('class', `fa ${data.icon}`);
        $('#details-popup').modal("show");
    }

All this is not anything special. Simple javascript, jquery, html programming.

Of course I wasn’t satisfied so I needed more options for the user. So using the right click you can get a dropdown with more options. This allows you to view the comments section on any app you choose as well as edit and delete functionality.


Right click drop-down menus

To do this is used a contextmenu plugin which I’m familiar with. Calling it was pretty straight forward.

      $.contextMenu({
            selector: '.app-icon', 
            callback: function(key, options, e) {
               if(key == "view"){
                    openAppDetails(elementId);
               }
               if(key == "edit"){
                   openEditModal(elementId);
               }
               if(key == 'delete'){
                    removeApp(elementId);
               }
            },
            items: {
                "view": {name: "View", icon: "fa-eye"},
                "edit": {name: "Edit", icon: "fa-pencil"},
                "sep1": "---------",
                "delete": {name: "Delete", icon:'fa-trash-o'}
            }
        });

To navigate our apps I added a search bar that would allow you to find an app quickly.

Search bar to filter apps

To do our search we add a listener to our input. If it is empty we display all the data otherwise we search using the search string from the input.

 $searchInput.on('input',function(e){
            var searchQuery = $(this).val();
            if(searchQuery == ""){
                getData();
            }else{
              searchData(searchQuery);
            }
        });

Lets look at the searchData function.

   function searchData(query){
        hideShowButton("show");
        var results = [];
        var toSearch = query;
        var objects = db.get('apps')
            .value("apps")
        for(var i=0; i<objects.length; i++) {
          for(key in objects[i]) {
            if(objects[i][key].toLowerCase().indexOf(toSearch.toLowerCase())!=-1) {
              results.push(objects[i]);
            }
          }
        }
        loadLinks(results);
    }

The main part of this function is looping through all our objects and searching for the values in them. If it finds a match it pushes the object to an array which is the search results returned. loadlinks is the function that displays all our apps.

For fun lets look at the loadlinks function which helps use generate the html layout for our application.

   function loadLinks(obj){
        let htmlLayout = "";
        let icons ="";
        for (var i = 0; i <= obj.length-1; i++) {
            icons += createIcon(obj[i]);
            if( ((i+1) % 3 ) == 0 ){
                htmlLayout += createHolder(icons);
                icons = "";
            }
            if( i == obj.length-1 && icons !="" ){
                htmlLayout += createHolder(icons);
            }
        }
        $('#app').html(htmlLayout);
        setListeners();
    }

The createIcon function generates the single app icons

   function createIcon(link){
        let layout = `<div class="4u 12u$(medium)">
            <section class="box app-icon" style="cursor:pointer;" 
            data-url="${link.url}" data-browser="${link.browser}" data-id="${link.id}">
                <i class="icon big rounded ${link.color} ${link.icon}"></i>
                <h3>${link.title}</h3>
                <p>${link.description}</p>
                <span class="fa ${getTypeIcon(link.browser)}"><i></i></span>
            </section>
        </div>`;
        return layout;
    }

The Database

So we have some options that we can do with our database. In fact we have alot of options. The database is basically a file. So we can have multiple files storing different kinds of apps, credentials or services as need be. The menu code is show below for the database option.

{
    label:"New Database",
    click () { mainWindow.webContents.send('new-file'); }
},
{
   label:"Switch Database",
   click () { mainWindow.webContents.send('select-file'); }
},
{
   label:"Copy Database",
   click () { mainWindow.webContents.send('copy-database'); }
},
{
   type: 'separator'
},
{
   label:"Reset Database",
   click () { mainWindow.webContents.send('reset-database'); }
},
{
   label:"Clear Database",
   click () { mainWindow.webContents.send('clear-apps'); }
},

Creating a new database

Lets look at the new database menu. It sends a new-file event. What we do is simple. We call the openSaveDialog function to get a file, we then set the file to our settings via config.db and we load the database and get the data.

   electron.ipcRenderer.on('new-file', (event, arg) => {
        var filename = openSaveDialog();
        if(filename){
            settings.set("config.db",filename);
            loadDb();
            getData();
        }
    });

Lets check out some of these functions. We can see how each works.

  function openSaveDialog(){
        var filename= dialog.showSaveDialog(
            { properties: ['selectFile'],filters: [
                { name: 'json', extensions: ['json'] },
              ]  });
        return filename;
    }

The openSaveDialog functions allows users to select a file. We can specify the extensions type as well. You can learn more about the showSaveDialog function here. Our function returns the filename we get from the showSaveDialog function which is part of the electron eco-system.

Lets look at the loadDb function which we use to load our json files to display our apps.

  function loadDb(){
        var dbFile = getDbSettings();
        adapter = new FileSync(dbFile);
        db = low(adapter);
        db.defaults({ apps: [],count: 0 })
            .write();
        $('#file-path').text( dbFile);
    }

We get the file from a function called getDbSettings. Once we create and FileSync object and add it to low. We then set the defaults. You can learn more about lowdb here. It will make more sense to check out the documentation.

Now we take a look at the getDbSettings function.

  function getDbSettings(){
        if(!settings.has("config.db")){
            var userDbfile = path.join(app.getPath('userData'),"db.json");
            settings.set("config",{db: userDbfile});
            return userDbfile;
        }else{
            return settings.get("config.db");
        }
    }

We first check to see if we have settings. If we do we just return the data. Otherwise we have to create the settings. The settings we create is called db and it holds the location of the database. We use app.getPath('userData') to get the windows default application settings path. We save our settings db.json in this location. We then set the location using settings.set("config",{db: userDbfile}) we can access this setting using settings.get("config.db").

All the functions might make it seem hard to follow but really all we are doing is creating a file if one doesn’t exist.

Switching Databases

We can switch between different databases. So we can create multiple files to store different kinds of apps. Or in a networked environment you can have different databases for different kinds of users. The menu option looks like below

{
    label:"Switch Database",
    click () { mainWindow.webContents.send('select-file'); }
},

We trigger an event called select-file. In our main logic we listen for this event to take action. This is shown below

   electron.ipcRenderer.on('select-file', (event, arg) => {
        var filename = openSelectFileDialog();
        if(filename){
            settings.set("config.db",filename);
            loadDb();
            getData();
        }
   });

To explain above we open a select file dialog that allows us to select a file. We return the filename and we set it in our configuration file has the main database now. Then we just run the loadDb and getData functions. This will load the new database and display the data in the database. Lets look at openSelectFileDialog to see what it does.

 function openSelectFileDialog(){
        var files = dialog.showOpenDialog(
            { properties: ['openFile'],  filters: [
                { name: 'json', extensions: ['json'] },
              ] });
         var filename = files[0];
         return filename;
    }

In the above we can see it uses the electron api and calls the function showOpenDialog with some options. It then returns the first file hence the files[0]. Check here for more information about dialog.

Final Notes

I have found electron to be a really cool interface to work with. This app current takes a while to load but I’m sure there are ways to speed that up I’m only touching the surface of what electron can do I think. The last thing I want to look at is opening web links. To do this in the app I used open [ learn more here ]. However there is another way. Open is definitely better and gives you more options. In the menu.js files I have a help menu that links to a webpage as shown below. I used require('electron').shell.openExternal( url ) to open the web link.

submenu: [
   {
      label: 'About',
      click () { mainWindow.webContents.send('show-about'); }
   },
   {
      label: 'Learn More',
      click () { require('electron').shell.openExternal('http://www.google.com') }
            },
 ]

The learn more label you can use to send the user to a documentation page. So they can learn more about the app.

Alrite thats it. Hope this helps you.

Appendix

I used other plugins too that I didn’t list above.

Choosing Icon color and adding comments
A sample app with icon and color

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s