diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..8497dbe8c2b5a7271ad351e434d575f5910d8d1b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pro.user
+lib/qtlib/
diff --git a/README.md b/README.md
index 80f4a988ff89a8de8df7e2cef3de15fa708b4e36..333c2b31d53916b03812c2fac36a66fe1020371d 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,24 @@
 # ocs-manager
 a tool to handle filemanagement and "apply"
+
+[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
+[![Build Status](https://travis-ci.org/opendesktop/ocs-manager.svg?branch=master)](https://travis-ci.org/opendesktop/ocs-manager)
+
+A management tool for ocs.
+
+Copyright: 2017, Opendesktop.org
+
+License: LGPL-3+
+
+
+## Note
+
+### Package depends
+
+* libqt5websockets5
+
+### Package depends (build)
+
+* build-essential
+* qt5-default
+* libqt5websockets5-dev
diff --git a/app/app.pri b/app/app.pri
new file mode 100644
index 0000000000000000000000000000000000000000..737e3eba34ea71dcc522e3e8a1f466a72023b313
--- /dev/null
+++ b/app/app.pri
@@ -0,0 +1,29 @@
+QT += \
+    core \
+    network \
+    websockets
+
+QT -= gui
+
+HEADERS += \
+    $${PWD}/src/websockets/websocketserver.h \
+    $${PWD}/src/handlers/confighandler.h \
+    $${PWD}/src/handlers/systemhandler.h \
+    $${PWD}/src/handlers/ocsapihandler.h \
+    $${PWD}/src/handlers/itemhandler.h
+
+SOURCES += \
+    $${PWD}/src/main.cpp \
+    $${PWD}/src/websockets/websocketserver.cpp \
+    $${PWD}/src/handlers/confighandler.cpp \
+    $${PWD}/src/handlers/systemhandler.cpp \
+    $${PWD}/src/handlers/ocsapihandler.cpp \
+    $${PWD}/src/handlers/itemhandler.cpp
+
+RESOURCES += $${PWD}/configs/configs.qrc
+
+INCLUDEPATH += $${PWD}/src
+
+unix:!ios:!android {
+    QT += dbus
+}
diff --git a/app/configs/application.json b/app/configs/application.json
new file mode 100644
index 0000000000000000000000000000000000000000..b61c09fd7cd3497720ebd33e1ad0a62f45cbe240
--- /dev/null
+++ b/app/configs/application.json
@@ -0,0 +1,13 @@
+{
+    "id": "ocs-manager",
+    "name": "ocs-manager",
+    "version": "0.0.0",
+    "organization": "Opendesktop.org",
+    "domain": "org.opendesktop.ocs-manager",
+    "icon": "",
+    "description": "A management tool for ocs.",
+    "license": "LGPL-3+",
+    "author": "Opendesktop.org",
+    "contact": "contact@opendesktop.org",
+    "homepage": "https://github.com/opendesktop/ocs-manager"
+}
diff --git a/app/configs/configs.qrc b/app/configs/configs.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..496e259d7a5937ae4a51fa761a76b84b33a05daf
--- /dev/null
+++ b/app/configs/configs.qrc
@@ -0,0 +1,7 @@
+<RCC>
+    <qresource prefix="/configs">
+        <file>application.json</file>
+        <file>install_types.json</file>
+        <file>install_types_alias.json</file>
+    </qresource>
+</RCC>
diff --git a/app/configs/install_types.json b/app/configs/install_types.json
new file mode 100644
index 0000000000000000000000000000000000000000..335de22350e81310a3820d1f0d79e6a5aea33037
--- /dev/null
+++ b/app/configs/install_types.json
@@ -0,0 +1,192 @@
+{
+    "bin": {
+        "name": "Softwares",
+        "destination": "$HOME/.local/bin",
+        "generic_destination": "$APP_DATA/bin"
+    },
+    "downloads": {
+        "name": "Downloads",
+        "destination": "$HOME/Downloads",
+        "generic_destination": "$APP_DATA/downloads"
+    },
+    "documents": {
+        "name": "Documents",
+        "destination": "$HOME/Documents",
+        "generic_destination": "$APP_DATA/documents"
+    },
+    "pictures": {
+        "name": "Pictures",
+        "destination": "$HOME/Pictures",
+        "generic_destination": "$APP_DATA/pictures"
+    },
+    "music": {
+        "name": "Music",
+        "destination": "$HOME/Music",
+        "generic_destination": "$APP_DATA/music"
+    },
+    "videos": {
+        "name": "Videos",
+        "destination": "$HOME/Videos",
+        "generic_destination": "$APP_DATA/videos"
+    },
+    "wallpapers": {
+        "name": "Wallpapers",
+        "destination": "$XDG_DATA_HOME/wallpapers",
+        "generic_destination": "$APP_DATA/wallpapers"
+    },
+    "fonts": {
+        "name": "Fonts",
+        "destination": "$HOME/.fonts",
+        "generic_destination": "$APP_DATA/fonts"
+    },
+    "cursors": {
+        "name": "Cursors",
+        "destination": "$HOME/.icons",
+        "generic_destination": "$APP_DATA/cursors"
+    },
+    "icons": {
+        "name": "Icons",
+        "destination": "$XDG_DATA_HOME/icons",
+        "generic_destination": "$APP_DATA/icons"
+    },
+    "emoticons": {
+        "name": "Emoticons",
+        "destination": "$XDG_DATA_HOME/emoticons",
+        "generic_destination": "$APP_DATA/emoticons"
+    },
+    "themes": {
+        "name": "Desktop Themes",
+        "destination": "$HOME/.themes",
+        "generic_destination": "$APP_DATA/themes"
+    },
+    "emerald_themes": {
+        "name": "Emerald Themes",
+        "destination": "$HOME/.emerald/themes",
+        "generic_destination": "$APP_DATA/emerald_themes"
+    },
+    "enlightenment_themes": {
+        "name": "Enlightenment Themes",
+        "destination": "$HOME/.e/e/themes",
+        "generic_destination": "$APP_DATA/enlightenment_themes"
+    },
+    "enlightenment_backgrounds": {
+        "name": "Enlightenment Backgrounds",
+        "destination": "$HOME/.e/e/backgrounds",
+        "generic_destination": "$APP_DATA/enlightenment_backgrounds"
+    },
+    "fluxbox_styles": {
+        "name": "Fluxbox Styles",
+        "destination": "$HOME/.fluxbox/styles",
+        "generic_destination": "$APP_DATA/fluxbox_styles"
+    },
+    "pekwm_themes": {
+        "name": "PekWM Themes",
+        "destination": "$HOME/.pekwm/themes",
+        "generic_destination": "$APP_DATA/pekwm_themes"
+    },
+    "icewm_themes": {
+        "name": "IceWM Themes",
+        "destination": "$HOME/.icewm/themes",
+        "generic_destination": "$APP_DATA/icewm_themes"
+    },
+    "plasma_plasmoids": {
+        "name": "Plasma Plasmoids",
+        "destination": "$XDG_DATA_HOME/plasma/plasmoids",
+        "generic_destination": "$APP_DATA/plasma_plasmoids"
+    },
+    "plasma_look_and_feel": {
+        "name": "Plasma Look and Feel",
+        "destination": "$XDG_DATA_HOME/plasma/look-and-feel",
+        "generic_destination": "$APP_DATA/plasma_look_and_feel"
+    },
+    "plasma_desktopthemes": {
+        "name": "Plasma Desktop Themes",
+        "destination": "$XDG_DATA_HOME/plasma/desktoptheme",
+        "generic_destination": "$APP_DATA/plasma_desktopthemes"
+    },
+    "kwin_effects": {
+        "name": "KWin Effects",
+        "destination": "$XDG_DATA_HOME/kwin/effects",
+        "generic_destination": "$APP_DATA/kwin_effects"
+    },
+    "kwin_scripts": {
+        "name": "KWin Scripts",
+        "destination": "$XDG_DATA_HOME/kwin/scripts",
+        "generic_destination": "$APP_DATA/kwin_scripts"
+    },
+    "kwin_tabbox": {
+        "name": "KWin Window Switcher",
+        "destination": "$XDG_DATA_HOME/kwin/tabbox",
+        "generic_destination": "$APP_DATA/kwin_tabbox"
+    },
+    "aurorae_themes": {
+        "name": "Aurorae Themes",
+        "destination": "$XDG_DATA_HOME/aurorae/themes",
+        "generic_destination": "$APP_DATA/aurorae_themes"
+    },
+    "dekorator_themes": {
+        "name": "deKorator Themes",
+        "destination": "$XDG_DATA_HOME/deKorator/themes",
+        "generic_destination": "$APP_DATA/dekorator_themes"
+    },
+    "qtcurve": {
+        "name": "QtCurve Themes",
+        "destination": "$XDG_DATA_HOME/QtCurve",
+        "generic_destination": "$APP_DATA/qtcurve"
+    },
+    "color_schemes": {
+        "name": "KDE Color Schemes",
+        "destination": "$XDG_DATA_HOME/color-schemes",
+        "generic_destination": "$APP_DATA/color_schemes"
+    },
+    "gnome_shell_extensions": {
+        "name": "Gnome Shell Extensions",
+        "destination": "$XDG_DATA_HOME/gnome-shell/extensions",
+        "generic_destination": "$APP_DATA/gnome_shell_extensions"
+    },
+    "cinnamon_applets": {
+        "name": "Cinnamon Applets",
+        "destination": "$XDG_DATA_HOME/cinnamon/applets",
+        "generic_destination": "$APP_DATA/cinnamon_applets"
+    },
+    "cinnamon_desklets": {
+        "name": "Cinnamon Desklets",
+        "destination": "$XDG_DATA_HOME/cinnamon/desklets",
+        "generic_destination": "$APP_DATA/cinnamon_desklets"
+    },
+    "cinnamon_extensions": {
+        "name": "Cinnamon Extensions",
+        "destination": "$XDG_DATA_HOME/cinnamon/extensions",
+        "generic_destination": "$APP_DATA/cinnamon_extensions"
+    },
+    "nautilus_scripts": {
+        "name": "Nautilus Scripts",
+        "destination": "$XDG_DATA_HOME/nautilus/scripts",
+        "generic_destination": "$APP_DATA/nautilus_scripts"
+    },
+    "amarok_scripts": {
+        "name": "Amarok Scripts",
+        "destination": "$KDEHOME/share/apps/amarok/scripts",
+        "generic_destination": "$APP_DATA/amarok_scripts"
+    },
+    "yakuake_skins": {
+        "name": "Yakuake Skins",
+        "destination": "$KDEHOME/share/apps/yakuake/skins",
+        "generic_destination": "$APP_DATA/yakuake_skins"
+    },
+    "cairo_clock_themes": {
+        "name": "Cairo-Clock Themes",
+        "destination": "$HOME/.cairo-clock/themes",
+        "generic_destination": "$APP_DATA/cairo_clock_themes"
+    },
+    "books": {
+        "name": "Books",
+        "destination": "$APP_DATA/books",
+        "generic_destination": "$APP_DATA/books"
+    },
+    "comics": {
+        "name": "Comics",
+        "destination": "$APP_DATA/comics",
+        "generic_destination": "$APP_DATA/comics"
+    }
+}
diff --git a/app/configs/install_types_alias.json b/app/configs/install_types_alias.json
new file mode 100644
index 0000000000000000000000000000000000000000..d67bb8bdd3d57e9ca16d759627fb6e2babed23ea
--- /dev/null
+++ b/app/configs/install_types_alias.json
@@ -0,0 +1,62 @@
+{
+    "gnome_shell_themes": {
+        "base": "themes",
+        "name": "Gnome Shell Themes"
+    },
+    "cinnamon_themes": {
+        "base": "themes",
+        "name": "Cinnamon Themes"
+    },
+    "gtk2_themes": {
+        "base": "themes",
+        "name": "GTK2 Themes"
+    },
+    "gtk3_themes": {
+        "base": "themes",
+        "name": "GTK3 Themes"
+    },
+    "metacity_themes": {
+        "base": "themes",
+        "name": "Metacity Themes"
+    },
+    "xfwm4_themes": {
+        "base": "themes",
+        "name": "XFWM4 Themes"
+    },
+    "openbox_themes": {
+        "base": "themes",
+        "name": "Openbox Themes"
+    },
+    "kvantum_themes": {
+        "base": "themes",
+        "name": "Kvantum Themes"
+    },
+    "compiz_themes": {
+        "base": "emerald_themes",
+        "name": "Compiz Themes"
+    },
+    "beryl_themes": {
+        "base": "emerald_themes",
+        "name": "Beryl Themes"
+    },
+    "plasma4_plasmoids": {
+        "base": "plasma_plasmoids",
+        "name": "Plasma4 Plasmoids"
+    },
+    "plasma5_plasmoids": {
+        "base": "plasma_plasmoids",
+        "name": "Plasma5 Plasmoids"
+    },
+    "plasma5_look_and_feel": {
+        "base": "plasma_look_and_feel",
+        "name": "Plasma5 Look and Feel"
+    },
+    "plasma5_desktopthemes": {
+        "base": "plasma_desktopthemes",
+        "name": "Plasma5 Desktop Themes"
+    },
+    "plasma_color_schemes": {
+        "base": "color_schemes",
+        "name": "Plasma Color Schemes"
+    }
+}
diff --git a/app/src/handlers/confighandler.cpp b/app/src/handlers/confighandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..88ad828f2a5fc01e9030bc3eab686656bf05dfa8
--- /dev/null
+++ b/app/src/handlers/confighandler.cpp
@@ -0,0 +1,202 @@
+#include "confighandler.h"
+
+#include <QStringList>
+
+#include "qtlib_dir.h"
+
+ConfigHandler::ConfigHandler(QObject *parent)
+    : QObject(parent)
+{
+    appConfig_ = qtlib::Config(":/configs");
+    usrConfig_ = qtlib::Config(qtlib::Dir::genericConfigPath() + "/" + getAppConfigApplication()["id"].toString());
+}
+
+QJsonObject ConfigHandler::getAppConfigApplication()
+{
+    if (appConfigApplication_.isEmpty()) {
+        appConfigApplication_ = appConfig_.get("application");
+    }
+    return appConfigApplication_;
+}
+
+QJsonObject ConfigHandler::getAppConfigInstallTypes()
+{
+    if (appConfigInstallTypes_.isEmpty()) {
+        QJsonObject installTypes = appConfig_.get("install_types");
+        foreach (const QString &key, installTypes.keys()) {
+            QJsonObject installtype = installTypes[key].toObject();
+            installtype["destination"] = convertPathString(installtype["destination"].toString());
+            installtype["generic_destination"] = convertPathString(installtype["generic_destination"].toString());
+            installTypes[key] = installtype;
+        }
+        QJsonObject installTypesAlias = appConfig_.get("install_types_alias");
+        foreach (const QString &key, installTypesAlias.keys()) {
+            QJsonObject installTypeAlias = installTypesAlias[key].toObject();
+            QString baseKey = installTypeAlias["base"].toString();
+            if (installTypes.contains(baseKey)) {
+                QJsonObject installType = installTypes[baseKey].toObject();
+                installType["base"] = baseKey;
+                installType["name"] = installTypeAlias["name"].toString();
+                installTypes[key] = installType;
+            }
+        }
+        appConfigInstallTypes_ = installTypes;
+    }
+    return appConfigInstallTypes_;
+}
+
+QJsonObject ConfigHandler::getUsrConfigApplication()
+{
+    return usrConfig_.get("application");
+}
+
+bool ConfigHandler::setUsrConfigApplication(const QJsonObject &object)
+{
+    return usrConfig_.set("application", object);
+}
+
+QJsonObject ConfigHandler::getUsrConfigProviders()
+{
+    return usrConfig_.get("providers");
+}
+
+bool ConfigHandler::setUsrConfigProviders(const QJsonObject &object)
+{
+    return usrConfig_.set("providers", object);
+}
+
+QJsonObject ConfigHandler::getUsrConfigCategories()
+{
+    return usrConfig_.get("categories");
+}
+
+bool ConfigHandler::setUsrConfigCategories(const QJsonObject &object)
+{
+    return usrConfig_.set("categories", object);
+}
+
+QJsonObject ConfigHandler::getUsrConfigInstalledItems()
+{
+    return usrConfig_.get("installed_items");
+}
+
+bool ConfigHandler::setUsrConfigInstalledItems(const QJsonObject &object)
+{
+    return usrConfig_.set("installed_items", object);
+}
+
+bool ConfigHandler::setUsrConfigProvidersProvider(const QString &providerKey, const QJsonObject &object)
+{
+    /* object format
+    {
+        "id": "example",
+        "location": "https://example.com/ocs/v1/",
+        "name": "Example",
+        "icon": "https://example.com/icon.png",
+        "termsofuse": "https://example.com/termsofuse",
+        "register": "https://example.com/register",
+        "_providerfile": "https://example.com/ocs/providers.xml"
+    }
+    */
+    QJsonObject providers = getUsrConfigProviders();
+    providers[providerKey] = object;
+    return setUsrConfigProviders(providers);
+}
+
+bool ConfigHandler::removeUsrConfigProvidersProvider(const QString &providerKey)
+{
+    QJsonObject providers = getUsrConfigProviders();
+    providers.remove(providerKey);
+    return setUsrConfigProviders(providers);
+}
+
+bool ConfigHandler::setUsrConfigCategoriesProvider(const QString &providerKey, const QJsonObject &object)
+{
+    /* object format
+    {
+        "1": {
+            "id": "1",
+            "name": "Desktop Icons",
+            "install_type": "icons"
+        },
+        "2": {
+            "id": "2",
+            "name": "Desktop Themes",
+            "install_type": "themes"
+        }
+    }
+    */
+    QJsonObject categories = getUsrConfigCategories();
+    categories[providerKey] = object;
+    return setUsrConfigCategories(categories);
+}
+
+bool ConfigHandler::removeUsrConfigCategoriesProvider(const QString &providerKey)
+{
+    QJsonObject categories = getUsrConfigCategories();
+    categories.remove(providerKey);
+    return setUsrConfigCategories(categories);
+}
+
+bool ConfigHandler::setUsrConfigCategoriesInstallType(const QString &providerKey, const QString &categoryKey, const QString &installType)
+{
+    QJsonObject categories = getUsrConfigCategories();
+    QJsonObject providerCategories;
+    if (categories.contains(providerKey)) {
+        providerCategories = categories[providerKey].toObject();
+    }
+    QJsonObject providerCategory;
+    if (providerCategories.contains(categoryKey)) {
+        providerCategory = providerCategories[categoryKey].toObject();
+    }
+    providerCategory["install_type"] = installType;
+    providerCategories[categoryKey] = providerCategory;
+    categories[providerKey] = providerCategories;
+    return setUsrConfigCategories(categories);
+}
+
+bool ConfigHandler::setUsrConfigInstalledItemsItem(const QString &itemKey, const QJsonObject &object)
+{
+    /* object format
+    {
+        "url": "http://example.com/downloads/123-1.tar.gz",
+        "filename": "123-1.tar.gz",
+        "install_type": "icons",
+        "provider": "http://example.com/ocs/v1/",
+        "content_id": "123",
+        "files": [
+            "iconset-light",
+            "iconset-dark"
+        ],
+        "installed_at": 1483658977219
+    }
+    */
+    QJsonObject installedItems = getUsrConfigInstalledItems();
+    installedItems[itemKey] = object;
+    return setUsrConfigInstalledItems(installedItems);
+}
+
+bool ConfigHandler::removeUsrConfigInstalledItemsItem(const QString &itemKey)
+{
+    QJsonObject installedItems = getUsrConfigInstalledItems();
+    installedItems.remove(itemKey);
+    return setUsrConfigInstalledItems(installedItems);
+}
+
+QString ConfigHandler::convertPathString(const QString &path)
+{
+    QString newPath = path;
+    if (newPath.contains("$HOME")) {
+        newPath.replace("$HOME", qtlib::Dir::homePath());
+    }
+    else if (newPath.contains("$XDG_DATA_HOME")) {
+        newPath.replace("$XDG_DATA_HOME", qtlib::Dir::genericDataPath());
+    }
+    else if (newPath.contains("$KDEHOME")) {
+        newPath.replace("$KDEHOME", qtlib::Dir::kdehomePath());
+    }
+    else if (newPath.contains("$APP_DATA")) {
+        newPath.replace("$APP_DATA", qtlib::Dir::genericDataPath() + "/" + getAppConfigApplication()["id"].toString());
+    }
+    return newPath;
+}
diff --git a/app/src/handlers/confighandler.h b/app/src/handlers/confighandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..ebaa18e14ec99f246c857d38e20fe7884309fe8f
--- /dev/null
+++ b/app/src/handlers/confighandler.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QObject>
+#include <QJsonObject>
+
+#include "qtlib_config.h"
+
+class ConfigHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit ConfigHandler(QObject *parent = 0);
+
+public slots:
+    QJsonObject getAppConfigApplication();
+    QJsonObject getAppConfigInstallTypes();
+
+    QJsonObject getUsrConfigApplication();
+    bool setUsrConfigApplication(const QJsonObject &object);
+    QJsonObject getUsrConfigProviders();
+    bool setUsrConfigProviders(const QJsonObject &object);
+    QJsonObject getUsrConfigCategories();
+    bool setUsrConfigCategories(const QJsonObject &object);
+    QJsonObject getUsrConfigInstalledItems();
+    bool setUsrConfigInstalledItems(const QJsonObject &object);
+
+    bool setUsrConfigProvidersProvider(const QString &providerKey, const QJsonObject &object);
+    bool removeUsrConfigProvidersProvider(const QString &providerKey);
+    bool setUsrConfigCategoriesProvider(const QString &providerKey, const QJsonObject &object);
+    bool removeUsrConfigCategoriesProvider(const QString &providerKey);
+    bool setUsrConfigCategoriesInstallType(const QString &providerKey, const QString &categoryKey, const QString &installType);
+    bool setUsrConfigInstalledItemsItem(const QString &itemKey, const QJsonObject &object);
+    bool removeUsrConfigInstalledItemsItem(const QString &itemKey);
+
+private:
+    QString convertPathString(const QString &path);
+
+    qtlib::Config appConfig_;
+    qtlib::Config usrConfig_;
+    QJsonObject appConfigApplication_;
+    QJsonObject appConfigInstallTypes_;
+};
diff --git a/app/src/handlers/itemhandler.cpp b/app/src/handlers/itemhandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e7e7c8273160ef63cd9963bd2968fe1713c58761
--- /dev/null
+++ b/app/src/handlers/itemhandler.cpp
@@ -0,0 +1,417 @@
+#include "itemhandler.h"
+
+#include <QUrlQuery>
+#include <QJsonValue>
+#include <QJsonArray>
+#include <QFileInfo>
+#include <QDateTime>
+
+#ifdef QTLIB_UNIX
+#include <QProcess>
+#endif
+
+#include "qtlib_dir.h"
+#include "qtlib_file.h"
+#include "qtlib_networkresource.h"
+#include "qtlib_package.h"
+
+#include "handlers/confighandler.h"
+
+ItemHandler::ItemHandler(ConfigHandler *configHandler, QObject *parent)
+    : QObject(parent), configHandler_(configHandler)
+{}
+
+QJsonObject ItemHandler::metadataSet() const
+{
+    return metadataSet_;
+}
+
+void ItemHandler::getItem(const QString &command, const QString &url, const QString &installType, const QString &filename,
+                          const QString &providerKey, const QString &contentId)
+{
+    // Use URL as unique key for metadata, network resource, and installed item
+    QString itemKey = url;
+
+    QJsonObject metadata;
+    metadata["command"] = command;
+    metadata["url"] = url;
+    metadata["install_type"] = installType;
+    metadata["filename"] = filename;
+    if (filename.isEmpty()) {
+        metadata["filename"] = QUrl(url).fileName();
+    }
+    metadata["provider"] = providerKey;
+    metadata["content_id"] = contentId;
+    metadata["files"] = QJsonArray();
+    metadata["installed_at"] = qint64();
+
+    QJsonObject result;
+    result["metadata"] = metadata;
+
+    QJsonObject itemMetadataSet = metadataSet();
+
+    if (itemMetadataSet.contains(itemKey)) {
+        result["status"] = QString("error_downloadstart");
+        result["message"] = tr("The file already downloading");
+        emit downloadStarted(result);
+        return;
+    }
+
+    itemMetadataSet[itemKey] = metadata;
+    setMetadataSet(itemMetadataSet);
+
+    qtlib::NetworkResource *resource = new qtlib::NetworkResource(itemKey, QUrl(url), true, this);
+    connect(resource, &qtlib::NetworkResource::downloadProgress, this, &ItemHandler::downloadProgress);
+    connect(resource, &qtlib::NetworkResource::finished, this, &ItemHandler::networkResourceFinished);
+    resource->get();
+
+    result["status"] = QString("success_downloadstart");
+    result["message"] = tr("Downloading");
+    emit downloadStarted(result);
+}
+
+void ItemHandler::getItemByOcsUrl(const QString &ocsUrl)
+{
+    QUrl ocsUrlObj(ocsUrl);
+    QUrlQuery query(ocsUrlObj);
+
+    QString scheme = "ocs";
+    QString command = "download";
+    QString url = "";
+    QString type = "downloads";
+    QString filename = "";
+
+    if (!ocsUrlObj.scheme().isEmpty()) {
+        scheme = ocsUrlObj.scheme();
+    }
+
+    if (!ocsUrlObj.host().isEmpty()) {
+        command = ocsUrlObj.host();
+    }
+
+    if (query.hasQueryItem("url") && !query.queryItemValue("url").isEmpty()) {
+        url = query.queryItemValue("url", QUrl::FullyDecoded);
+    }
+
+    if (query.hasQueryItem("type") && !query.queryItemValue("type").isEmpty()) {
+        type = query.queryItemValue("type", QUrl::FullyDecoded);
+    }
+
+    if (query.hasQueryItem("filename") && !query.queryItemValue("filename").isEmpty()) {
+        filename = QUrl(query.queryItemValue("filename", QUrl::FullyDecoded)).fileName();
+    }
+
+    if (!url.isEmpty() && filename.isEmpty()) {
+        filename = QUrl(url).fileName();
+    }
+
+    // Still support xdg and xdgs schemes for backward compatibility
+    if ((scheme == "ocs" || scheme == "ocss" || scheme == "xdg" || scheme == "xdgs")
+            && (command == "download" || command == "install")
+            && QUrl(url).isValid()
+            && configHandler_->getAppConfigInstallTypes().contains(type)
+            && !filename.isEmpty()) {
+        getItem(command, url, type, filename);
+    }
+    else {
+        QJsonObject result;
+        result["status"] = QString("error_downloadstart");
+        result["message"] = tr("Invalid OCS-URL");
+        emit downloadStarted(result);
+    }
+}
+
+void ItemHandler::uninstall(const QString &itemKey)
+{
+    QJsonObject result;
+    result["status"] = QString("success_uninstallstart");
+    result["message"] = tr("Uninstalling");
+    emit uninstallStarted(result);
+
+    QJsonObject installedItem = configHandler_->getUsrConfigInstalledItems()[itemKey].toObject();
+    QString installType = installedItem["install_type"].toString();
+
+    qtlib::Dir destDir;
+#ifdef QTLIB_UNIX
+    destDir.setPath(configHandler_->getAppConfigInstallTypes()[installType].toObject()["destination"].toString());
+
+    foreach (const QJsonValue &filename, installedItem["files"].toArray()) {
+        QFileInfo fileInfo(destDir.path() + "/" + filename.toString());
+
+        // plasmapkg: Installation process has should be saved plasmapkg into destination directory
+
+        qtlib::Package package(fileInfo.filePath());
+
+        // Uninstall
+        if (installType == "bin") {
+            if (fileInfo.filePath().endsWith(".appimage", Qt::CaseInsensitive)) {
+                QProcess process;
+                process.start(fileInfo.filePath() + " --remove-appimage-desktop-integration");
+                process.waitForFinished();
+            }
+        }
+        else if (installType == "plasma_plasmoids" || installType == "plasma4_plasmoids" || installType == "plasma5_plasmoids") {
+            package.uninstallAsPlasmapkg("plasmoid");
+        }
+        else if (installType == "plasma_look_and_feel" || installType == "plasma5_look_and_feel") {
+            package.uninstallAsPlasmapkg("lookandfeel");
+        }
+        else if (installType == "plasma_desktopthemes" || installType == "plasma5_desktopthemes") {
+            package.uninstallAsPlasmapkg("theme");
+        }
+        else if (installType == "kwin_effects") {
+            package.uninstallAsPlasmapkg("kwineffect");
+        }
+        else if (installType == "kwin_scripts") {
+            package.uninstallAsPlasmapkg("kwinscript");
+        }
+        else if (installType == "kwin_tabbox") {
+            package.uninstallAsPlasmapkg("windowswitcher");
+        }
+
+        // Remove file
+        if (fileInfo.isDir()) {
+            qtlib::Dir(fileInfo.filePath()).remove();
+        }
+        else {
+            qtlib::File(fileInfo.filePath()).remove();
+        }
+    }
+#else
+    destDir.setPath(configHandler_->getAppConfigInstallTypes()[installType].toObject()["generic_destination"].toString());
+
+    foreach (const QJsonValue &filename, installedItem["files"].toArray()) {
+        QFileInfo fileInfo(destDir.path() + "/" + filename.toString());
+        if (fileInfo.isDir()) {
+            qtlib::Dir(fileInfo.filePath()).remove();
+        }
+        else {
+            qtlib::File(fileInfo.filePath()).remove();
+        }
+    }
+#endif
+
+    configHandler_->removeUsrConfigInstalledItemsItem(itemKey);
+
+    result["status"] = QString("success_uninstall");
+    result["message"] = tr("Uninstalled");
+    emit uninstallFinished(result);
+}
+
+void ItemHandler::networkResourceFinished(qtlib::NetworkResource *resource)
+{
+    QString itemKey = resource->id();
+
+    QJsonObject itemMetadataSet = metadataSet();
+    QJsonObject metadata = itemMetadataSet[itemKey].toObject();
+
+    QJsonObject result;
+    result["metadata"] = metadata;
+
+    if (!resource->isFinishedWithNoError()) {
+        itemMetadataSet.remove(itemKey);
+        setMetadataSet(itemMetadataSet);
+        result["status"] = QString("error_download");
+        result["message"] = resource->reply()->errorString();
+        emit downloadFinished(result);
+        resource->deleteLater();
+        return;
+    }
+
+    result["status"] = QString("success_download");
+    result["message"] = tr("Downloaded");
+    emit downloadFinished(result);
+
+    if (metadata["command"].toString() == "download") {
+        saveDownloadedFile(resource);
+    }
+    else if (metadata["command"].toString() == "install") {
+        installDownloadedFile(resource);
+    }
+}
+
+void ItemHandler::setMetadataSet(const QJsonObject &metadataSet)
+{
+    metadataSet_ = metadataSet;
+    emit metadataSetChanged();
+}
+
+void ItemHandler::saveDownloadedFile(qtlib::NetworkResource *resource)
+{
+    QString itemKey = resource->id();
+
+    QJsonObject itemMetadataSet = metadataSet();
+    QJsonObject metadata = itemMetadataSet[itemKey].toObject();
+
+    itemMetadataSet.remove(itemKey);
+    setMetadataSet(itemMetadataSet);
+
+    QJsonObject result;
+    result["metadata"] = metadata;
+    result["status"] = QString("success_savestart");
+    result["message"] = tr("Saving");
+    emit saveStarted(result);
+
+    QString filename = metadata["filename"].toString();
+    QString installType = metadata["install_type"].toString();
+
+    qtlib::Dir destDir(configHandler_->getAppConfigInstallTypes()[installType].toObject()["destination"].toString());
+    destDir.make();
+    qtlib::File destFile(destDir.path() + "/" + filename);
+
+    if (!resource->saveData(destFile.path())) {
+        result["status"] = QString("error_save");
+        result["message"] = tr("Failed to save data");
+        emit saveFinished(result);
+        resource->deleteLater();
+        return;
+    }
+
+    result["status"] = QString("success_save");
+    result["message"] = tr("Saved");
+    emit saveFinished(result);
+
+    resource->deleteLater();
+}
+
+void ItemHandler::installDownloadedFile(qtlib::NetworkResource *resource)
+{
+    // Installation pre-process
+    QString itemKey = resource->id();
+
+    QJsonObject itemMetadataSet = metadataSet();
+    QJsonObject metadata = itemMetadataSet[itemKey].toObject();
+
+    itemMetadataSet.remove(itemKey);
+    setMetadataSet(itemMetadataSet);
+
+    QJsonObject result;
+    result["metadata"] = metadata;
+    result["status"] = QString("success_savestart");
+    result["message"] = tr("Saving");
+    emit saveStarted(result);
+
+    QString filename = metadata["filename"].toString();
+    QString installType = metadata["install_type"].toString();
+
+    QString prefix = configHandler_->getAppConfigApplication()["id"].toString() + "_" + filename;
+    qtlib::Dir tempDir(qtlib::Dir::tempPath() + "/" + prefix);
+    tempDir.make();
+    qtlib::Dir tempDestDir(tempDir.path() + "/dest");
+    tempDestDir.make();
+    qtlib::Package package(tempDir.path() + "/" + filename);
+
+    if (!resource->saveData(package.path())) {
+        result["status"] = QString("error_save");
+        result["message"] = tr("Failed to save data");
+        emit saveFinished(result);
+        tempDir.remove();
+        resource->deleteLater();
+        return;
+    }
+
+    result["status"] = QString("success_save");
+    result["message"] = tr("Saved");
+    emit saveFinished(result);
+
+    // Installation main-process
+    result["status"] = QString("success_installstart");
+    result["message"] = tr("Installing");
+    emit installStarted(result);
+
+    qtlib::Dir destDir;
+#ifdef QTLIB_UNIX
+    destDir.setPath(configHandler_->getAppConfigInstallTypes()[installType].toObject()["destination"].toString());
+
+    // plasmapkg: Need to save package to remove installed files later
+
+    if (installType == "bin"
+            && package.installAsProgram(tempDestDir.path() + "/" + filename)) {
+        result["message"] = tr("The file has been installed as program");
+    }
+    else if ((installType == "plasma_plasmoids" || installType == "plasma4_plasmoids" || installType == "plasma5_plasmoids")
+             && package.installAsPlasmapkg("plasmoid")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The plasmoid has been installed");
+    }
+    else if ((installType == "plasma_look_and_feel" || installType == "plasma5_look_and_feel")
+             && package.installAsPlasmapkg("lookandfeel")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The plasma look and feel has been installed");
+    }
+    else if ((installType == "plasma_desktopthemes" || installType == "plasma5_desktopthemes")
+             && package.installAsPlasmapkg("theme")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The plasma desktop theme has been installed");
+    }
+    else if (installType == "kwin_effects"
+             && package.installAsPlasmapkg("kwineffect")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The KWin effect has been installed");
+    }
+    else if (installType == "kwin_scripts"
+             && package.installAsPlasmapkg("kwinscript")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The KWin script has been installed");
+    }
+    else if (installType == "kwin_tabbox"
+             && package.installAsPlasmapkg("windowswitcher")) {
+        package.installAsFile(tempDestDir.path() + "/" + filename);
+        result["message"] = tr("The KWin window switcher has been installed");
+    }
+    else if (package.installAsArchive(tempDestDir.path())) {
+        result["message"] = tr("The archive file has been extracted");
+    }
+    else if (package.installAsFile(tempDestDir.path() + "/" + filename)) {
+        result["message"] = tr("The file has been installed");
+    }
+    else {
+        result["status"] = QString("error_install");
+        result["message"] = tr("Failed to installation");
+        emit installFinished(result);
+        tempDir.remove();
+        resource->deleteLater();
+        return;
+    }
+#else
+    destDir.setPath(configHandler_->getAppConfigInstallTypes()[installType].toObject()["generic_destination"].toString());
+
+    if (qtlib::File(package.path()).copy(tempDestDir.path() + "/" + filename)) {
+        result["message"] = tr("The file has been installed");
+    }
+    else {
+        result["status"] = QString("error_install");
+        result["message"] = tr("Failed to installation");
+        emit installFinished(result);
+        tempDir.remove();
+        resource->deleteLater();
+        return;
+    }
+#endif
+
+    destDir.make();
+
+    QJsonArray installedFiles;
+    foreach (const QFileInfo &fileInfo, tempDestDir.list()) {
+        installedFiles.append(QJsonValue(fileInfo.fileName()));
+        if (fileInfo.isDir()) {
+            qtlib::Dir(fileInfo.filePath()).move(destDir.path() + "/" + fileInfo.fileName());
+        }
+        else {
+            qtlib::File(fileInfo.filePath()).move(destDir.path() + "/" + fileInfo.fileName());
+        }
+    }
+
+    // Installation post-process
+    metadata.remove("command");
+    metadata["files"] = installedFiles;
+    metadata["installed_at"] = QDateTime::currentMSecsSinceEpoch();
+    configHandler_->setUsrConfigInstalledItemsItem(itemKey, metadata);
+
+    result["metadata"] = metadata;
+    result["status"] = QString("success_install");
+    emit installFinished(result);
+
+    tempDir.remove();
+    resource->deleteLater();
+}
diff --git a/app/src/handlers/itemhandler.h b/app/src/handlers/itemhandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..5f378c4dbc385704a2a7a5c69890b8b585e23f66
--- /dev/null
+++ b/app/src/handlers/itemhandler.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <QObject>
+#include <QJsonObject>
+
+namespace qtlib {
+class NetworkResource;
+}
+
+class ConfigHandler;
+
+class ItemHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit ItemHandler(ConfigHandler *configHandler, QObject *parent = 0);
+
+signals:
+    void metadataSetChanged();
+    void downloadStarted(QJsonObject result);
+    void downloadFinished(QJsonObject result);
+    void downloadProgress(QString id, qint64 bytesReceived, qint64 bytesTotal);
+    void saveStarted(QJsonObject result);
+    void saveFinished(QJsonObject result);
+    void installStarted(QJsonObject result);
+    void installFinished(QJsonObject result);
+    void uninstallStarted(QJsonObject result);
+    void uninstallFinished(QJsonObject result);
+
+public slots:
+    QJsonObject metadataSet() const;
+
+    void getItem(const QString &command, const QString &url, const QString &installType, const QString &filename = "",
+                 const QString &providerKey = "", const QString &contentId = "");
+    void getItemByOcsUrl(const QString &ocsUrl);
+    void uninstall(const QString &itemKey);
+
+private slots:
+    void networkResourceFinished(qtlib::NetworkResource *resource);
+
+private:
+    void setMetadataSet(const QJsonObject &metadataSet);
+
+    void saveDownloadedFile(qtlib::NetworkResource *resource);
+    void installDownloadedFile(qtlib::NetworkResource *resource);
+
+    ConfigHandler *configHandler_;
+    QJsonObject metadataSet_;
+};
diff --git a/app/src/handlers/ocsapihandler.cpp b/app/src/handlers/ocsapihandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..310ed88a5e7fe20c2a858b7a23de316fe341867d
--- /dev/null
+++ b/app/src/handlers/ocsapihandler.cpp
@@ -0,0 +1,203 @@
+#include "ocsapihandler.h"
+
+#include <QJsonValue>
+
+#include "qtlib_ocsapi.h"
+
+#include "handlers/confighandler.h"
+
+OcsApiHandler::OcsApiHandler(ConfigHandler *configHandler, QObject *parent)
+    : QObject(parent), configHandler_(configHandler)
+{}
+
+bool OcsApiHandler::addProviders(const QString &providerFileUrl)
+{
+    QJsonArray providers = qtlib::OcsApi::getProviderFile(QUrl(providerFileUrl));
+    if (!providers.isEmpty()) {
+        foreach (const QJsonValue &providerValue, providers) {
+            QJsonObject provider = providerValue.toObject();
+            if (provider.contains("location")) {
+                // Use location (API base URL) as unique key
+                QString providerKey = provider["location"].toString();
+                if (configHandler_->setUsrConfigProvidersProvider(providerKey, provider)) {
+                    updateCategories(providerKey, true);
+                }
+            }
+        }
+        return true;
+    }
+    return false;
+}
+
+bool OcsApiHandler::removeProvider(const QString &providerKey)
+{
+    if (configHandler_->removeUsrConfigProvidersProvider(providerKey)) {
+        configHandler_->removeUsrConfigCategoriesProvider(providerKey);
+        return true;
+    }
+    return false;
+}
+
+bool OcsApiHandler::updateAllCategories(bool force)
+{
+    QJsonObject providers = configHandler_->getUsrConfigProviders();
+    if (!providers.isEmpty()) {
+        foreach (const QString &providerKey, providers.keys()) {
+            updateCategories(providerKey, force);
+        }
+        return true;
+    }
+    return false;
+}
+
+bool OcsApiHandler::updateCategories(const QString &providerKey, bool force)
+{
+    QJsonObject providers = configHandler_->getUsrConfigProviders();
+
+    if (!providers.contains(providerKey)) {
+        return false;
+    }
+
+    QString baseUrl = providers[providerKey].toObject()["location"].toString();
+    QJsonObject response = qtlib::OcsApi(baseUrl, QUrl(baseUrl)).getContentCategories();
+
+    if (!response.contains("data")) {
+        return false;
+    }
+
+    // Data type variation workaround, convert object to array
+    QJsonArray responseData;
+    if (response["data"].isObject()) {
+        foreach (const QJsonValue &dataValue, response["data"].toObject()) {
+            responseData.append(dataValue);
+        }
+    }
+    else {
+        responseData = response["data"].toArray();
+    }
+
+    QJsonObject installTypes = configHandler_->getAppConfigInstallTypes();
+
+    QJsonObject categories = configHandler_->getUsrConfigCategories();
+    QJsonObject providerCategories;
+    if (!force && categories.contains(providerKey)) {
+        providerCategories = categories[providerKey].toObject();
+    }
+
+    QJsonObject newProviderCategories;
+    foreach (const QJsonValue &dataValue, responseData) {
+        QJsonObject data = dataValue.toObject();
+
+        // Data type variation workaround, convert int to string
+        QString id;
+        if (data["id"].isString()) {
+            id = data["id"].toString();
+        }
+        else {
+            id = QString::number(data["id"].toInt());
+        }
+
+        // Use category id as unique key
+        QString categoryKey = id;
+
+        QString name = data["name"].toString();
+        // display_name: Not compatible to legacy OCS-API
+        if (data.contains("display_name") && data["display_name"].toString() != "") {
+            name = data["display_name"].toString();
+        }
+
+        // parent_id: Not compatible to legacy OCS-API
+        QString parentId = "";
+        if (data.contains("parent_id") && data["parent_id"].toString() != "") {
+            parentId = data["parent_id"].toString();
+        }
+
+        QString installType = configHandler_->getAppConfigApplication()["options"].toObject()["default_install_type"].toString();
+        if (!force && providerCategories.contains(categoryKey)) {
+            installType = providerCategories[categoryKey].toObject()["install_type"].toString();
+        }
+        // xdg_type: Not compatible to legacy OCS-API
+        else if (data.contains("xdg_type") && data["xdg_type"].toString() != ""
+                && installTypes.contains(data["xdg_type"].toString())) {
+            installType = data["xdg_type"].toString();
+        }
+
+        QJsonObject category;
+        category["id"] = id;
+        category["name"] = name;
+        category["parent_id"] = parentId;
+        category["install_type"] = installType;
+        newProviderCategories[categoryKey] = category;
+    }
+
+    return configHandler_->setUsrConfigCategoriesProvider(providerKey, newProviderCategories);
+}
+
+QJsonObject OcsApiHandler::getContents(const QString &providerKeys, const QString &categoryKeys,
+                                       const QString &xdgTypes, const QString &packageTypes,
+                                       const QString &search, const QString &sortmode, int pagesize, int page)
+{
+    QJsonObject responseSet;
+
+    QStringList providerKeyList;
+    if (!providerKeys.isEmpty()) {
+        providerKeyList = providerKeys.split(",");
+    }
+
+    QStringList categoryKeyList;
+    if (!categoryKeys.isEmpty()) {
+        categoryKeyList = categoryKeys.split(",");
+    }
+
+    QJsonObject providers = configHandler_->getUsrConfigProviders();
+    QJsonObject categories = configHandler_->getUsrConfigCategories();
+
+    foreach (const QString &providerKey, providers.keys()) {
+        if (!providerKeyList.isEmpty() && !providerKeyList.contains(providerKey)) {
+            continue;
+        }
+        QStringList categoryIdList;
+        QJsonObject providerCategories = categories[providerKey].toObject();
+        foreach (const QString &categoryKey, providerCategories.keys()) {
+            if (!categoryKeyList.isEmpty() && !categoryKeyList.contains(categoryKey)) {
+                continue;
+            }
+            categoryIdList.append(providerCategories[categoryKey].toObject()["id"].toString());
+        }
+        if (!categoryIdList.isEmpty()) {
+            QString baseUrl = providers[providerKey].toObject()["location"].toString();
+            QUrlQuery query;
+            // categories: Comma-separated list is not compatible to legacy OCS-API
+            //query.addQueryItem("categories", categoryIdList.join(","));
+            query.addQueryItem("categories", categoryIdList.join("x"));
+            // xdg_types: Not compatible to legacy OCS-API
+            if (!xdgTypes.isEmpty()) {
+                query.addQueryItem("xdg_types", xdgTypes);
+            }
+            // package_types: Not compatible to legacy OCS-API
+            if (!packageTypes.isEmpty()) {
+                query.addQueryItem("package_types", packageTypes);
+            }
+            if (!search.isEmpty()) {
+                query.addQueryItem("search", search);
+            }
+            query.addQueryItem("sortmode", sortmode);
+            query.addQueryItem("pagesize", QString::number(pagesize));
+            query.addQueryItem("page", QString::number(page));
+            responseSet[providerKey] = qtlib::OcsApi(baseUrl, QUrl(baseUrl)).getContentDataSet(query);
+        }
+    }
+
+    return responseSet;
+}
+
+QJsonObject OcsApiHandler::getContent(const QString &providerKey, const QString &contentId)
+{
+    QJsonObject response;
+    QJsonObject providers = configHandler_->getUsrConfigProviders();
+    if (providers.contains(providerKey)) {
+        QString baseUrl = providers[providerKey].toObject()["location"].toString();
+        response = qtlib::OcsApi(baseUrl, QUrl(baseUrl)).getContentData(contentId);
+    }
+    return response;
+}
diff --git a/app/src/handlers/ocsapihandler.h b/app/src/handlers/ocsapihandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..4e4d3095e50f985c7c39f392eaccc85bdc719e5a
--- /dev/null
+++ b/app/src/handlers/ocsapihandler.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <QObject>
+#include <QJsonObject>
+
+class ConfigHandler;
+
+class OcsApiHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit OcsApiHandler(ConfigHandler *configHandler, QObject *parent = 0);
+
+public slots:
+    bool addProviders(const QString &providerFileUrl);
+    bool removeProvider(const QString &providerKey);
+    bool updateAllCategories(bool force = false);
+    bool updateCategories(const QString &providerKey, bool force = false);
+    QJsonObject getContents(const QString &providerKeys = "", const QString &categoryKeys = "",
+                            const QString &xdgTypes = "", const QString &packageTypes = "",
+                            const QString &search = "", const QString &sortmode = "new", int pagesize = 25, int page = 0);
+    QJsonObject getContent(const QString &providerKey, const QString &contentId);
+
+private:
+    ConfigHandler *configHandler_;
+};
diff --git a/app/src/handlers/systemhandler.cpp b/app/src/handlers/systemhandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b7b95ae612ad117f55182713ee9ec9f7b9211a91
--- /dev/null
+++ b/app/src/handlers/systemhandler.cpp
@@ -0,0 +1,204 @@
+#include "systemhandler.h"
+
+#ifdef QTLIB_UNIX
+#include <QFileInfo>
+#include <QProcess>
+#include <QDBusMessage>
+#include <QDBusConnection>
+#include <QDBusVariant>
+#include <QDebug>
+#endif
+
+SystemHandler::SystemHandler(QObject *parent)
+    : QObject(parent)
+{}
+
+bool SystemHandler::isUnix()
+{
+#ifdef QTLIB_UNIX
+    return true;
+#endif
+    return false;
+}
+
+QString SystemHandler::desktopEnvironment()
+{
+    QString desktop = "unknown";
+    QString currentDesktop = "";
+
+    if (!qgetenv("XDG_CURRENT_DESKTOP").isEmpty()) {
+        currentDesktop = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP").constData()).toLower();
+    }
+    else if (!qgetenv("XDG_SESSION_DESKTOP").isEmpty()) {
+        currentDesktop = QString::fromLocal8Bit(qgetenv("XDG_SESSION_DESKTOP").constData()).toLower();
+    }
+    else if (!qgetenv("DESKTOP_SESSION").isEmpty()) {
+        currentDesktop = QString::fromLocal8Bit(qgetenv("DESKTOP_SESSION").constData()).toLower();
+    }
+
+    if (currentDesktop.contains("kde") || currentDesktop.contains("plasma")) {
+        desktop = "kde";
+    }
+    else if (currentDesktop.contains("gnome") || currentDesktop.contains("unity")) {
+        desktop = "gnome";
+    }
+    else if (currentDesktop.contains("xfce")) {
+        desktop = "xfce";
+    }
+    return desktop;
+}
+
+bool SystemHandler::isApplicableType(const QString &installType)
+{
+    QString desktop = desktopEnvironment();
+
+    if (installType == "wallpapers"
+            && (desktop == "kde" || desktop == "gnome" || desktop == "xfce")) {
+        return true;
+    }
+    /*else if (installType == "icons"
+             && (desktop == "kde" || desktop == "gnome" || desktop == "xfce")) {
+        return true;
+    }
+    else if (installType == "cursors"
+             && (desktop == "kde" || desktop == "gnome" || desktop == "xfce")) {
+        return true;
+    }
+    else if ((installType == "aurorae_themes" && desktop == "kde")
+             || (installType == "metacity_themes" && desktop == "gnome")
+             || (installType == "xfwm4_themes" && desktop == "xfce")) {
+        return true;
+    }*/
+    return false;
+}
+
+#ifdef QTLIB_UNIX
+bool SystemHandler::applyFile(const QString &path, const QString &installType)
+{
+    if (QFileInfo::exists(path) && isApplicableType(installType)) {
+        if (installType == "wallpapers") {
+            return applyWallpaper(path);
+        }
+        /*else if (installType == "icons") {
+            return applyIcon(path);
+        }
+        else if (installType == "cursors") {
+            return applyCursor(path);
+        }
+        else if (installType == "aurorae_themes"
+                 || installType == "metacity_themes"
+                 || installType == "xfwm4_themes") {
+            return applyWindowTheme(path);
+        }*/
+    }
+    return false;
+}
+#endif
+
+#ifdef QTLIB_UNIX
+bool SystemHandler::applyWallpaper(const QString &path)
+{
+    QString desktop = desktopEnvironment();
+
+    if (desktop == "kde") {
+        // plasma5.6+
+        QDBusMessage message = QDBusMessage::createMethodCall("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "evaluateScript");
+        QVariantList arguments;
+
+        QString script;
+        QTextStream out(&script);
+        out << "for (var key in desktops()) {"
+            << "var d = desktops()[key];"
+            << "d.wallpaperPlugin = 'org.kde.image';"
+            << "d.currentConfigGroup = ['Wallpaper', 'org.kde.image', 'General'];"
+            << "d.writeConfig('Image', 'file://" + path + "');"
+            << "}";
+
+        arguments << QVariant(script);
+        message.setArguments(arguments);
+
+        QDBusMessage reply = QDBusConnection::sessionBus().call(message);
+
+        if (reply.type() == QDBusMessage::ErrorMessage) {
+            qWarning() << reply.errorMessage();
+            return false;
+        }
+        return true;
+    }
+    else if (desktop == "gnome") {
+        QStringList arguments;
+        // gnome3
+        arguments << "set" << "org.gnome.desktop.background" << "picture-uri" << "file://" + path;
+        return QProcess::startDetached("gsettings", arguments);
+        // gnome2
+        //arguments << "--type=string" << "--set" << "/desktop/gnome/background/picture_filename" << path;
+        //return QProcess::startDetached("gconftool-2", arguments);
+    }
+    else if (desktop == "xfce") {
+        QDBusMessage message = QDBusMessage::createMethodCall("org.xfce.Xfconf", "/org/xfce/Xfconf", "org.xfce.Xfconf", "SetProperty");
+        QVariantList arguments;
+
+        QString channelValue = "xfce4-desktop";
+        //QString propertyValue = "/backdrop/screen0/monitor0/image-path";
+        QString propertyValue = "/backdrop/screen0/monitor0/workspace0/last-image";
+        QDBusVariant valueValue(path);
+
+        arguments << QVariant(channelValue) << QVariant(propertyValue) << QVariant::fromValue(valueValue);
+        message.setArguments(arguments);
+
+        QDBusMessage reply = QDBusConnection::sessionBus().call(message);
+
+        if (reply.type() == QDBusMessage::ErrorMessage) {
+            qWarning() << reply.errorMessage();
+            return false;
+        }
+        return true;
+    }
+    return false;
+}
+
+bool SystemHandler::applyIcon(const QString &path)
+{
+    qDebug() << path;
+
+    QString desktop = desktopEnvironment();
+
+    if (desktop == "kde") {
+    }
+    else if (desktop == "gnome") {
+    }
+    else if (desktop == "xfce") {
+    }
+    return false;
+}
+
+bool SystemHandler::applyCursor(const QString &path)
+{
+    qDebug() << path;
+
+    QString desktop = desktopEnvironment();
+
+    if (desktop == "kde") {
+    }
+    else if (desktop == "gnome") {
+    }
+    else if (desktop == "xfce") {
+    }
+    return false;
+}
+
+bool SystemHandler::applyWindowTheme(const QString &path)
+{
+    qDebug() << path;
+
+    QString desktop = desktopEnvironment();
+
+    if (desktop == "kde") {
+    }
+    else if (desktop == "gnome") {
+    }
+    else if (desktop == "xfce") {
+    }
+    return false;
+}
+#endif
diff --git a/app/src/handlers/systemhandler.h b/app/src/handlers/systemhandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..a49f9a6a6b7a1aff0ee9e9036eea2a40a8e625cd
--- /dev/null
+++ b/app/src/handlers/systemhandler.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QObject>
+
+class SystemHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit SystemHandler(QObject *parent = 0);
+
+public slots:
+    bool isUnix();
+
+    QString desktopEnvironment();
+    bool isApplicableType(const QString &installType);
+
+#ifdef QTLIB_UNIX
+    bool applyFile(const QString &path, const QString &installType);
+#endif
+
+private:
+#ifdef QTLIB_UNIX
+    bool applyWallpaper(const QString &path);
+    bool applyIcon(const QString &path);
+    bool applyCursor(const QString &path);
+    bool applyWindowTheme(const QString &path);
+#endif
+};
diff --git a/app/src/main.cpp b/app/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e6c228c2964fb470b73bea8df5603877082b6eb1
--- /dev/null
+++ b/app/src/main.cpp
@@ -0,0 +1,60 @@
+#include <QStringList>
+#include <QJsonObject>
+//#include <QTranslator>
+//#include <QLocale>
+#include <QCommandLineParser>
+#include <QCommandLineOption>
+#include <QCoreApplication>
+#include <QDebug>
+
+#include "handlers/confighandler.h"
+#include "websockets/websocketserver.h"
+
+int main(int argc, char *argv[])
+{
+    // Init
+    QCoreApplication app(argc, argv);
+
+    ConfigHandler *configHandler = new ConfigHandler();
+    QJsonObject appConfigApplication = configHandler->getAppConfigApplication();
+
+    app.setApplicationName(appConfigApplication["name"].toString());
+    app.setApplicationVersion(appConfigApplication["version"].toString());
+    app.setOrganizationName(appConfigApplication["organization"].toString());
+    app.setOrganizationDomain(appConfigApplication["domain"].toString());
+
+    // Setup translator
+    //QTranslator translator;
+    //if (translator.load(QLocale(), "messages", ".", ":/i18n")) {
+    //    app.installTranslator(&translator);
+    //}
+
+    // Setup CLI
+    QCommandLineParser clParser;
+    clParser.setApplicationDescription(appConfigApplication["description"].toString());
+    clParser.addHelpOption();
+    clParser.addVersionOption();
+
+    // Port 49152-61000 will available as ephemeral port
+    // https://en.wikipedia.org/wiki/Ephemeral_port
+    QCommandLineOption clOptionPort(QStringList() << "p" << "port", "Port for websocket server [default: 49152].", "port", "49152");
+    clParser.addOption(clOptionPort);
+
+    clParser.process(app);
+
+    int port = clParser.value(clOptionPort).toInt();
+
+    // Setup websocket server
+    WebSocketServer *wsServer = new WebSocketServer(configHandler, appConfigApplication["id"].toString(), port, &app);
+    QObject::connect(wsServer, &WebSocketServer::stopped, &app, &QCoreApplication::quit);
+
+    if (wsServer->start()) {
+        qDebug() << "Websocket server started at:" << wsServer->serverUrl().toString();
+    }
+    else {
+        qCritical() << "Failed to start websocket server:" << wsServer->errorString();
+        return 1;
+    }
+
+    return app.exec();
+}
diff --git a/app/src/websockets/websocketserver.cpp b/app/src/websockets/websocketserver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..857c9e9e7496aee7551c855bd63aabad11c3ba5f
--- /dev/null
+++ b/app/src/websockets/websocketserver.cpp
@@ -0,0 +1,347 @@
+#include "websocketserver.h"
+
+#include <QHostAddress>
+#include <QWebSocketServer>
+#include <QWebSocket>
+
+#include "qtlib_json.h"
+
+#include "handlers/confighandler.h"
+#include "handlers/systemhandler.h"
+#include "handlers/ocsapihandler.h"
+#include "handlers/itemhandler.h"
+
+WebSocketServer::WebSocketServer(ConfigHandler *configHandler, const QString &serverName, quint16 serverPort, QObject *parent)
+    : QObject(parent), configHandler_(configHandler), serverName_(serverName), serverPort_(serverPort)
+{
+    wsServer_ = new QWebSocketServer(serverName_, QWebSocketServer::NonSecureMode, this);
+    connect(wsServer_, &QWebSocketServer::newConnection, this, &WebSocketServer::wsNewConnection);
+    connect(wsServer_, &QWebSocketServer::closed, this, &WebSocketServer::stopped);
+
+    configHandler_->setParent(this);
+    systemHandler_ = new SystemHandler(this);
+    ocsApiHandler_ = new OcsApiHandler(configHandler_, this);
+    itemHandler_ = new ItemHandler(configHandler_, this);
+    connect(itemHandler_, &ItemHandler::metadataSetChanged, this, &WebSocketServer::itemMetadataSetChanged);
+    connect(itemHandler_, &ItemHandler::downloadStarted, this, &WebSocketServer::itemDownloadStarted);
+    connect(itemHandler_, &ItemHandler::downloadFinished, this, &WebSocketServer::itemDownloadFinished);
+    connect(itemHandler_, &ItemHandler::downloadProgress, this, &WebSocketServer::itemDownloadProgress);
+    connect(itemHandler_, &ItemHandler::saveStarted, this, &WebSocketServer::itemSaveStarted);
+    connect(itemHandler_, &ItemHandler::saveFinished, this, &WebSocketServer::itemSaveFinished);
+    connect(itemHandler_, &ItemHandler::installStarted, this, &WebSocketServer::itemInstallStarted);
+    connect(itemHandler_, &ItemHandler::installFinished, this, &WebSocketServer::itemInstallFinished);
+    connect(itemHandler_, &ItemHandler::uninstallStarted, this, &WebSocketServer::itemUninstallStarted);
+    connect(itemHandler_, &ItemHandler::uninstallFinished, this, &WebSocketServer::itemUninstallFinished);
+}
+
+WebSocketServer::~WebSocketServer()
+{
+    stop();
+    wsServer_->deleteLater();
+}
+
+bool WebSocketServer::start()
+{
+    if (wsServer_->listen(QHostAddress::Any, serverPort_)) {
+        emit started();
+        return true;
+    }
+    return false;
+}
+
+void WebSocketServer::stop()
+{
+    wsServer_->close();
+}
+
+bool WebSocketServer::isError()
+{
+    if (wsServer_->error() != QWebSocketProtocol::CloseCodeNormal) {
+        return true;
+    }
+    return false;
+}
+
+QString WebSocketServer::errorString()
+{
+    return wsServer_->errorString();
+}
+
+QUrl WebSocketServer::serverUrl()
+{
+    return wsServer_->serverUrl();
+}
+
+void WebSocketServer::wsNewConnection()
+{
+    QWebSocket *wsClient = wsServer_->nextPendingConnection();
+    connect(wsClient, &QWebSocket::disconnected, this, &WebSocketServer::wsDisconnected);
+    connect(wsClient, &QWebSocket::textMessageReceived, this, &WebSocketServer::wsTextMessageReceived);
+    connect(wsClient, &QWebSocket::binaryMessageReceived, this, &WebSocketServer::wsBinaryMessageReceived);
+    wsClients_ << wsClient;
+}
+
+void WebSocketServer::wsDisconnected()
+{
+    QWebSocket *wsClient = qobject_cast<QWebSocket *>(sender());
+    if (wsClient) {
+        wsClients_.removeAll(wsClient);
+        wsClient->deleteLater();
+    }
+}
+
+void WebSocketServer::wsTextMessageReceived(const QString &message)
+{
+    QWebSocket *wsClient = qobject_cast<QWebSocket *>(sender());
+    if (wsClient) {
+        qtlib::Json json(message.toUtf8());
+        if (json.isObject()) {
+            QJsonObject object = json.toObject();
+            receiveMessage(object["id"].toString(), object["func"].toString(), object["data"].toArray());
+        }
+    }
+}
+
+void WebSocketServer::wsBinaryMessageReceived(const QByteArray &message)
+{
+    QWebSocket *wsClient = qobject_cast<QWebSocket *>(sender());
+    if (wsClient) {
+        qtlib::Json json(message);
+        if (json.isObject()) {
+            QJsonObject object = json.toObject();
+            receiveMessage(object["id"].toString(), object["func"].toString(), object["data"].toArray());
+        }
+    }
+}
+
+void WebSocketServer::itemMetadataSetChanged()
+{
+    QJsonArray data;
+    sendMessage("", "ItemHandler::metadataSetChanged", data);
+}
+
+void WebSocketServer::itemDownloadStarted(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::downloadStarted", data);
+}
+
+void WebSocketServer::itemDownloadFinished(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::downloadFinished", data);
+}
+
+void WebSocketServer::itemDownloadProgress(QString id, qint64 bytesReceived, qint64 bytesTotal)
+{
+    QJsonArray data;
+    data.append(id);
+    data.append(bytesReceived);
+    data.append(bytesTotal);
+    sendMessage("", "ItemHandler::downloadProgress", data);
+}
+
+void WebSocketServer::itemSaveStarted(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::saveStarted", data);
+}
+
+void WebSocketServer::itemSaveFinished(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::saveFinished", data);
+}
+
+void WebSocketServer::itemInstallStarted(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::installStarted", data);
+}
+
+void WebSocketServer::itemInstallFinished(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::installFinished", data);
+}
+
+void WebSocketServer::itemUninstallStarted(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::uninstallStarted", data);
+}
+
+void WebSocketServer::itemUninstallFinished(QJsonObject result)
+{
+    QJsonArray data;
+    data.append(result);
+    sendMessage("", "ItemHandler::uninstallFinished", data);
+}
+
+void WebSocketServer::receiveMessage(const QString &id, const QString &func, const QJsonArray &data)
+{
+    /* message object format
+    {
+        "id": "example",
+        "func": "functionName",
+        "data": ["value", 2, true]
+    }
+    */
+
+    QJsonArray resultData;
+
+    // WebSocketServer
+    if (func == "WebSocketServer::stop") {
+        stop();
+    }
+    else if (func == "WebSocketServer::isError") {
+        resultData.append(isError());
+    }
+    else if (func == "WebSocketServer::errorString") {
+        resultData.append(errorString());
+    }
+    else if (func == "WebSocketServer::serverUrl") {
+        resultData.append(serverUrl().toString());
+    }
+    // ConfigHandler
+    else if (func == "ConfigHandler::getAppConfigApplication") {
+        resultData.append(configHandler_->getAppConfigApplication());
+    }
+    else if (func == "ConfigHandler::getAppConfigInstallTypes") {
+        resultData.append(configHandler_->getAppConfigInstallTypes());
+    }
+    else if (func == "ConfigHandler::getUsrConfigApplication") {
+        resultData.append(configHandler_->getUsrConfigApplication());
+    }
+    else if (func == "ConfigHandler::setUsrConfigApplication") {
+        resultData.append(configHandler_->setUsrConfigApplication(data.at(0).toObject()));
+    }
+    else if (func == "ConfigHandler::getUsrConfigProviders") {
+        resultData.append(configHandler_->getUsrConfigProviders());
+    }
+    else if (func == "ConfigHandler::setUsrConfigProviders") {
+        resultData.append(configHandler_->setUsrConfigProviders(data.at(0).toObject()));
+    }
+    else if (func == "ConfigHandler::getUsrConfigCategories") {
+        resultData.append(configHandler_->getUsrConfigCategories());
+    }
+    else if (func == "ConfigHandler::setUsrConfigCategories") {
+        resultData.append(configHandler_->setUsrConfigCategories(data.at(0).toObject()));
+    }
+    else if (func == "ConfigHandler::getUsrConfigInstalledItems") {
+        resultData.append(configHandler_->getUsrConfigInstalledItems());
+    }
+    else if (func == "ConfigHandler::setUsrConfigInstalledItems") {
+        resultData.append(configHandler_->setUsrConfigInstalledItems(data.at(0).toObject()));
+    }
+    else if (func == "ConfigHandler::setUsrConfigProvidersProvider") {
+        resultData.append(configHandler_->setUsrConfigProvidersProvider(data.at(0).toString(), data.at(1).toObject()));
+    }
+    else if (func == "ConfigHandler::removeUsrConfigProvidersProvider") {
+        resultData.append(configHandler_->removeUsrConfigProvidersProvider(data.at(0).toString()));
+    }
+    else if (func == "ConfigHandler::setUsrConfigCategoriesProvider") {
+        resultData.append(configHandler_->setUsrConfigCategoriesProvider(data.at(0).toString(), data.at(1).toObject()));
+    }
+    else if (func == "ConfigHandler::removeUsrConfigCategoriesProvider") {
+        resultData.append(configHandler_->removeUsrConfigCategoriesProvider(data.at(0).toString()));
+    }
+    else if (func == "ConfigHandler::setUsrConfigCategoriesInstallType") {
+        resultData.append(configHandler_->setUsrConfigCategoriesInstallType(data.at(0).toString(), data.at(1).toString(), data.at(2).toString()));
+    }
+    else if (func == "ConfigHandler::setUsrConfigInstalledItemsItem") {
+        resultData.append(configHandler_->setUsrConfigInstalledItemsItem(data.at(0).toString(), data.at(1).toObject()));
+    }
+    else if (func == "ConfigHandler::removeUsrConfigInstalledItemsItem") {
+        resultData.append(configHandler_->removeUsrConfigInstalledItemsItem(data.at(0).toString()));
+    }
+    // SystemHandler
+    else if (func == "SystemHandler::isUnix") {
+        resultData.append(systemHandler_->isUnix());
+    }
+    else if (func == "SystemHandler::desktopEnvironment") {
+        resultData.append(systemHandler_->desktopEnvironment());
+    }
+    else if (func == "SystemHandler::isApplicableType") {
+        resultData.append(systemHandler_->isApplicableType(data.at(0).toString()));
+    }
+    else if (func == "SystemHandler::applyFile") {
+#ifdef QTLIB_UNIX
+        resultData.append(systemHandler_->applyFile(data.at(0).toString(), data.at(1).toString()));
+#else
+        resultData.append(false);
+#endif
+    }
+    // OcsApiHandler
+    else if (func == "OcsApiHandler::addProviders") {
+        resultData.append(ocsApiHandler_->addProviders(data.at(0).toString()));
+    }
+    else if (func == "OcsApiHandler::removeProvider") {
+        resultData.append(ocsApiHandler_->removeProvider(data.at(0).toString()));
+    }
+    else if (func == "OcsApiHandler::updateAllCategories") {
+        resultData.append(ocsApiHandler_->updateAllCategories(data.at(0).toBool()));
+    }
+    else if (func == "OcsApiHandler::updateCategories") {
+        resultData.append(ocsApiHandler_->updateCategories(data.at(0).toString(), data.at(1).toBool()));
+    }
+    else if (func == "OcsApiHandler::getContents") {
+        resultData.append(ocsApiHandler_->getContents(data.at(0).toString(), data.at(1).toString(),
+                                                      data.at(2).toString(), data.at(3).toString(),
+                                                      data.at(4).toString(), data.at(5).toString(), data.at(6).toInt(), data.at(7).toInt()));
+    }
+    else if (func == "OcsApiHandler::getContent") {
+        resultData.append(ocsApiHandler_->getContent(data.at(0).toString(), data.at(1).toString()));
+    }
+    // ItemHandler
+    else if (func == "ItemHandler::metadataSet") {
+        resultData.append(itemHandler_->metadataSet());
+    }
+    else if (func == "ItemHandler::getItem") {
+        itemHandler_->getItem(data.at(0).toString(), data.at(1).toString(), data.at(2).toString(), data.at(3).toString(),
+                              data.at(4).toString(), data.at(5).toString());
+    }
+    else if (func == "ItemHandler::getItemByOcsUrl") {
+        itemHandler_->getItemByOcsUrl(data.at(0).toString());
+    }
+    else if (func == "ItemHandler::uninstall") {
+        itemHandler_->uninstall(data.at(0).toString());
+    }
+    // Not supported
+    else {
+        return;
+    }
+
+    sendMessage(id, func, resultData);
+}
+
+void WebSocketServer::sendMessage(const QString &id, const QString &func, const QJsonArray &data)
+{
+    /* message object format
+    {
+        "id": "example",
+        "func": "functionName",
+        "data": ["value", 2, true]
+    }
+    */
+
+    QJsonObject object;
+    object["id"] = id;
+    object["func"] = func;
+    object["data"] = data;
+
+    QByteArray binaryMessage = qtlib::Json(object).toJson();
+    QString textMessage = QString::fromUtf8(binaryMessage);
+
+    foreach (QWebSocket *wsClient, wsClients_) {
+        wsClient->sendTextMessage(textMessage);
+        //wsClient->sendBinaryMessage(binaryMessage);
+    }
+}
diff --git a/app/src/websockets/websocketserver.h b/app/src/websockets/websocketserver.h
new file mode 100644
index 0000000000000000000000000000000000000000..6bcebf7c28fb7d6ee4987b04ff3a89ab378f0eff
--- /dev/null
+++ b/app/src/websockets/websocketserver.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <QObject>
+#include <QUrl>
+#include <QJsonObject>
+#include <QJsonArray>
+
+class QWebSocketServer;
+class QWebSocket;
+
+class ConfigHandler;
+class SystemHandler;
+class OcsApiHandler;
+class ItemHandler;
+
+class WebSocketServer : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit WebSocketServer(ConfigHandler *configHandler, const QString &serverName = "WebSocketServer", quint16 serverPort = 0, QObject *parent = 0);
+    ~WebSocketServer();
+
+signals:
+    void started();
+    void stopped();
+
+public slots:
+    bool start();
+    void stop();
+    bool isError();
+    QString errorString();
+    QUrl serverUrl();
+
+private slots:
+    void wsNewConnection();
+    void wsDisconnected();
+    void wsTextMessageReceived(const QString &message);
+    void wsBinaryMessageReceived(const QByteArray &message);
+
+    void itemMetadataSetChanged();
+    void itemDownloadStarted(QJsonObject result);
+    void itemDownloadFinished(QJsonObject result);
+    void itemDownloadProgress(QString id, qint64 bytesReceived, qint64 bytesTotal);
+    void itemSaveStarted(QJsonObject result);
+    void itemSaveFinished(QJsonObject result);
+    void itemInstallStarted(QJsonObject result);
+    void itemInstallFinished(QJsonObject result);
+    void itemUninstallStarted(QJsonObject result);
+    void itemUninstallFinished(QJsonObject result);
+
+private:
+    void receiveMessage(const QString &id, const QString &func, const QJsonArray &data);
+    void sendMessage(const QString &id, const QString &func, const QJsonArray &data);
+
+    ConfigHandler *configHandler_;
+    SystemHandler *systemHandler_;
+    OcsApiHandler *ocsApiHandler_;
+    ItemHandler *itemHandler_;
+
+    QString serverName_;
+    quint16 serverPort_;
+    QWebSocketServer *wsServer_;
+    QList<QWebSocket *> wsClients_;
+};
diff --git a/lib/lib.pri b/lib/lib.pri
new file mode 100644
index 0000000000000000000000000000000000000000..5343ea67e320f54da6851de3969e590c456d432d
--- /dev/null
+++ b/lib/lib.pri
@@ -0,0 +1 @@
+include($${PWD}/qtlib/qtlib.pri)
diff --git a/ocs-manager.pro b/ocs-manager.pro
new file mode 100644
index 0000000000000000000000000000000000000000..f1df8facfdf0c4aa4a68db2cf30d7e1ae4d708f7
--- /dev/null
+++ b/ocs-manager.pro
@@ -0,0 +1,19 @@
+message("Please execute scripts/import.sh for build dependencies")
+
+TARGET = ocs-manager
+
+TEMPLATE = app
+
+CONFIG += \
+    c++11 \
+    console
+
+CONFIG -= app_bundle
+
+DEFINES += QT_DEPRECATED_WARNINGS
+
+DISTFILES += $${PWD}/README.md
+
+include($${PWD}/lib/lib.pri)
+include($${PWD}/app/app.pri)
+include($${PWD}/scripts/scripts.pri)
diff --git a/scripts/import.sh b/scripts/import.sh
new file mode 100644
index 0000000000000000000000000000000000000000..50992b4f529fc2ae523927a50bae7cfcca4b9d6f
--- /dev/null
+++ b/scripts/import.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+################################################################################
+# This is utility script to import build dependencies
+################################################################################
+
+PROJDIR="$(cd "$(dirname "${0}")/../" && pwd)"
+
+if [ ! -d "${PROJDIR}/lib/qtlib" ]; then
+    git clone https://github.com/akiraohgaki/qtlib.git -b release-0.1.0 --single-branch --depth=1 "${PROJDIR}/lib/qtlib"
+fi
diff --git a/scripts/scripts.pri b/scripts/scripts.pri
new file mode 100644
index 0000000000000000000000000000000000000000..47e7b4bf65cbaf037ad2933caa803bbe7b7582d6
--- /dev/null
+++ b/scripts/scripts.pri
@@ -0,0 +1 @@
+DISTFILES += $${PWD}/import.sh