We all use various applications every day. We run them on different platforms including mobile, web, desktop, tablet, TV. Each software company decides for itself which platforms its app should support.

There are cases when limiting to just one platform is not an option, and it’s necessary to make the application available on different platforms. Developing separate applications for each platform requires a lot of time and effort. It’s necessary to create a separate team of developers for each platform, and this is quite expensive. In addition, it’s necessary to maintain each application separately, which also requires additional resources.

We all know that the most popular platform at the moment is the web. According to Top Website Statistics For 2023, there are about 1.13 billion websites on the Internet in 2023. A new website is built every three seconds.

It is not surprising that many companies prefer to initially create a web application and then convert it into a mobile or desktop application. Such applications look like regular native applications, but in fact, they actually display a web app or its special version.

This approach to supporting multiple platforms saves software companies and developers a significant amount of time and money. It allows the companies to quickly create a mobile or desktop application and test its demand on these platforms before investing more in the development of separate applications for each platform using native development tools.

In this article, I will talk about when it makes sense to convert a web app or website to a desktop app and how to do it professionally using Molybden.

When to convert a web app to a desktop app

First of all, let’s talk about when we should to convert a web app to a desktop app.

I believe there’s no point in creating a desktop application that merely displays a web app or website. It wouldn’t be fair to ask a user to download and install your desktop app when they wouldn’t gain any additional value from it compared to simply visiting a URL in their favorite web browser.

If a user wants to quickly open a web app or website with a click on the Taskbar (Windows, Linux) or Dock (macOS) item or from a shortcut on their Desktop, they can create a shortcut for the website in Google Chrome or download the PWA version of the web application if available.

It makes sense to convert a web app to a desktop app when you can enhance the capabilities of the web application through integration with the desktop and operating system. For instance, this can include access to file system, access to hardware, displaying native desktop notifications, launching on startup, running command line utilities, automating specific scenarios within the web application, etc.

How to convert a web app to a desktop app

To convert any web application or website to a modern cross-platform desktop application, you can use Molybden.

What’s Molybden

Molybden is an SDK for building modern cross-platform desktop apps. It provides a set of tools and frameworks that simplifies the cross-platform desktop app development process, enabling you to build, test, and deploy cross-platform desktop apps faster. It allows software companies to save time and money by reusing the same codebase across all platforms.

One of the special features of Molybden is its use of Chromium for rendering the user interface within the application’s windows. This allows you to construct the entire user interface of the application using web technologies or to load and display any modern web app or website directly within the desktop application.

Moreover, Molybden provides a framework that allows web applications to interact with low-level native APIs and make use of the operating system’s capabilities. This greatly extends the capabilities of web applications and removes the limitations typically associated with running web applications in a standard web browser.

In the next sections, I will show you how to convert a web app to a desktop app using Molybden.

Generating a project from a template

First of all, we need to generate a project. Molybden offers a special utility called create-molybden-app. This is the official project scaffolding tool that allows you to create a project based on a specific template. To generate a project, execute the following command:

npm create molybden-app@latest

Specify your application’s name, choose the Website project template, and provide the URL of your web application:

? Project name: PurePhotos
? Select project template: Website
? Enter the website URL: https://purephotos.app/my/dashboard

Done! To get started run:
cd PurePhotos
npm install
npm run molybden dev

Running the app

Once the project is generated, you can execute the following commands to build and run your desktop application:

cd PurePhotos
npm install
npm run molybden dev

After launching, you will see a login form to access the web application:

Pure Photos app login page

Pure Photos app dashboard page

About Pure Photos

In this example, we are converting the web app called Pure Photos. It allows you to remove any background from your photos and hide any imperfections from your photo automatically.

With Pure Photos you can:

  • Find persons in photos and align their heads to be on the same levels across the whole batch.
  • Correct exposure and colors.
  • Detect noise and remove it.
  • Get rid of pimples, freckles, and spots.
  • Generate a layered image in Adobe Photoshop Document (APD) file format.

Working from IDE

Let’s extend our desktop application with some features.

Molybden generates a project in a format that is supported by all modern C++ IDEs such as MS Visual Studio, Qt Creator, and CLion.

To make changes to the application code, I open the project in CLion (my favorite IDE) and select the src-cpp/src/main.cc file. This file contains the main function that is executed when the application is launched. In this file we can implement the logic of our desktop application.

Molybden project in CLion

Displaying the app in system tray

Let’s make our application appear in the system tray. For this, we will create and configure application tray as shown in the following code:

#include "molybden.hpp"

using namespace molybden;

void launch() {
  App::init([](std::shared_ptr<App> app) {
    // Configure app tray.
    auto tray = Tray::create(app);
    tray->setImage(app->getPath(PathKey::kAppResources) + "/imageTemplate.png");
    tray->setTooltip("Pure Photos");
    tray->setMenu(menu::Menu({
      menu::Item("About Pure Photos", [app](const CustomMenuItemActionArgs& args) {
        app->desktop()->openUrl("https://purephotos.app");
      }),
      menu::Separator(),
      menu::MacQuitApp()
    }));

    // Display app window.
    auto browser = Browser::create(app);
    browser->loadUrl(app->baseUrl());
    browser->show();
  });
}

Pure Photos app tray

Showing native desktop notifications

Our web application notifies the user when photo processing is completed. In a standard web browser, the user must grant the web application with permission to display desktop notifications. Molybden allows you to configure this programmatically.

Let’s allow the web application to always display native desktop notifications:

// Grant desktop notifications permission.
auto permissions = app->profile()->permissions();
permissions->onRequestPermission = [](const RequestPermissionArgs& args,
                                      RequestPermissionAction action) {
  if (args.permission_type == PermissionType::kNotifications) {
    action.grant();
  } else {
    action.ask();
  }
};

Accessing native API from web app

Let’s inform the web application that it is running inside our desktop application. In this case, the web application can display additional elements of the user interface or adapt its internal logic. For this purpose, we will create a specific variable that the web application can check to understand where it is running (in a standard web browser or the desktop application):

browser->onInjectJs = [](const InjectJsArgs& args, InjectJsAction action) {
  auto window = args.frame->executeJavaScript("window").asJsObject();
  window->putProperty("isDesktop", true);
  action.proceed();
};

The web application can access this variable in JavaScript as follows:

if (isDesktop !== undefined && isDesktop) {
    // The web app is running in desktop app.
}

Now, when the web application can understand that it’s running inside our desktop application, it can access various capabilities of the operating system. Let’s implement the ability to display a native Open Folder dialog and obtain the list of all files in the selected folder.

Molybden allows you to define a function in the desktop application and “inject” it into the web application so that it can be called directly from JavaScript.

Let’s declare a function that will show a native Open Folder dialog to the user and return the list of files in the selected folder as a JSON string:

std::string ShowOpenDialogAndGetDirFiles(std::shared_ptr<App> app) {
  std::promise<std::string> dir_path;

  // Show open dialog that allows the user to select a directory.
  OpenDialogOptions options;
  options.features.allow_multiple_selections = false;
  options.selection_policy = OpenDialogSelectionPolicy::kDirectories;
  OpenDialog::show(app, options, [&](const OpenDialogResult& result) {
    if (!result.paths.empty()) {
      dir_path.set_value(result.paths[0]);
    }
  });
  std::filesystem::path dir = dir_path.get_future().get();

  // Return the files of the selected directory as a JSON string.
  std::string result = "{ \"files\": [";
  if (std::filesystem::is_directory(dir)) {
    for (const auto& entry : std::filesystem::directory_iterator(dir)) {
      if (std::filesystem::is_regular_file(entry)) {
        result += "\"" + entry.path().string() + "\", ";
      }
    }
  }
  result += "]}";
  return result;
}

Register this function in JavaScript:

window->putProperty("GetDirectoryFiles", [app]() -> std::string {
  return ShowOpenDialogAndGetDirFiles(app);
});

You can now call this function from the JavaScript code of the web application:

if (isDesktop !== undefined && isDesktop) {
    console.log(GetDirectoryFiles());
}

After calling this JavaScript code from the web application, you will see the following output in the JavaScript Console:

Pure Photos app Open Folder dialog

Pure Photos app JavaScript Console

Customizing app icon

To customize the app icon, we can take the official Pure Photos logo, convert it to the required format, and use it to replace the default app icon.

Launch the application and make sure the new icon is being used:

Pure Photos app icon

Generating app installer

Once the all necessary features are implemented in our desktop application, we are ready to deliver it to end-users. For a smooth user experience, Molybden automatically generates an application installer in the native format for the current operating system.

To build the production version of the desktop application and generate an app installer, execute the following command:

npm run molybden build

The installer will be placed in the ./build-dist/pack directory.

Pure Photos DMG

It’s recommended that you sign and notarize your application before distributing it to end-users. It’s because macOS require all applications to be signed and notarized to be able to run them.

That’s it! With Molybden, you can extend your desktop application with many other features, but covering them all would make this article too extensive.

By the way, you can convert not only your own web app or website ;)

Full example

Here’s full source code of the created desktop application:

#include <filesystem>
#include <future>

#include "molybden.hpp"

using namespace molybden;

/**
  * Show open dialog and return the files of the selected directory as a JSON.
  */
std::string ShowOpenDialogAndGetDirFiles(std::shared_ptr<App> app) {
  std::promise<std::string> dir_path;

  // Show open dialog that allows the user to select a directory.
  OpenDialogOptions options;
  options.features.allow_multiple_selections = false;
  options.selection_policy = OpenDialogSelectionPolicy::kDirectories;
  OpenDialog::show(app, options, [&](const OpenDialogResult& result) {
    if (!result.paths.empty()) {
      dir_path.set_value(result.paths[0]);
    }
  });
  std::filesystem::path dir = dir_path.get_future().get();

  // Return the files of the selected directory as a JSON string.
  std::string result = "{ \"files\": [";
  if (std::filesystem::is_directory(dir)) {
    for (const auto& entry : std::filesystem::directory_iterator(dir)) {
      if (std::filesystem::is_regular_file(entry)) {
        result += "\"" + entry.path().string() + "\", ";
      }
    }
  }
  result += "]}";
  return result;
}

void launch() {
  App::init([](std::shared_ptr<App> app) {
    // Configure app tray.
    auto tray = Tray::create(app);
    tray->setImage(app->getPath(PathKey::kAppResources) + "/imageTemplate.png");
    tray->setTooltip("Pure Photos");
    tray->setMenu(menu::Menu({
      menu::Item("About Pure Photos", [app](const CustomMenuItemActionArgs& args) {
        app->desktop()->openUrl("https://purephotos.app");
      }),
      menu::Separator(),
      menu::MacQuitApp()
    }));

    // Grant desktop notifications permission.
    auto permissions = app->profile()->permissions();
    permissions->onRequestPermission = [](const RequestPermissionArgs& args,
                                          RequestPermissionAction action) {
      if (args.permission_type == PermissionType::kNotifications) {
        action.grant();
      } else {
        action.ask();
      }
    };

    // Display app window.
    auto browser = Browser::create(app);
    browser->onInjectJs = [app](const InjectJsArgs& args,
                                InjectJsAction action) {
      auto window = args.frame->executeJavaScript("window").asJsObject();
      // Inject the variable that indicates that this is a desktop app.
      window->putProperty("isDesktop", true);
      // Inject the function that returns the directory files.
      window->putProperty("GetDirectoryFiles", [app]() -> std::string {
        return ShowOpenDialogAndGetDirFiles(app);
      });
      action.proceed();
    };
    browser->loadUrl(app->baseUrl());
    browser->show();
  });
}

Pure Photos app

What’s next

Convert your web app to a desktop app with Molybden in minutes. Make sure on your own how Molybden simplifies the development process and let you build modern and beautiful cross-platform desktop apps in less time.

If you have any questions or need expert guidance, just let us know. We’ll be delighted to assist your company in creating a modern, beautiful, and functional cross-platform desktop app.