diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..75c107bcc90038bcdd0d643b20eb4d33a13b4245
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pro.user
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9665ad052a0e094cef3f3e0ee6fec3c4b509e5e5..0000000000000000000000000000000000000000
--- a/Makefile
+++ /dev/null
@@ -1,40 +0,0 @@
-SHELL = /bin/sh
-
-TARGET = xdgurl
-srcdir = ./src
-
-DESTDIR =
-prefix = /usr/local
-exec_prefix = $(prefix)
-bindir = $(exec_prefix)/bin
-datadir = $(prefix)/share
-
-INSTALL = install
-INSTALL_PROGRAM = $(INSTALL) -D -m 755
-INSTALL_DATA = $(INSTALL) -D -m 644
-RM = rm -f
-
-.PHONY: all rebuild build clean install uninstall
-
-all: rebuild ;
-
-rebuild: clean build ;
-
-build: $(TARGET) ;
-
-clean:
-	$(RM) ./$(TARGET)
-
-install: build
-	$(INSTALL_PROGRAM) ./$(TARGET) $(DESTDIR)$(bindir)/$(TARGET)
-	$(INSTALL_DATA) $(srcdir)/desktop/$(TARGET).desktop $(DESTDIR)$(datadir)/applications/$(TARGET).desktop
-	$(INSTALL_DATA) $(srcdir)/desktop/$(TARGET).svg $(DESTDIR)$(datadir)/icons/hicolor/scalable/apps/$(TARGET).svg
-
-uninstall:
-	$(RM) $(DESTDIR)$(bindir)/$(TARGET)
-	$(RM) $(DESTDIR)$(datadir)/applications/$(TARGET).desktop
-	$(RM) $(DESTDIR)$(datadir)/icons/hicolor/scalable/apps/$(TARGET).svg
-
-$(TARGET):
-	# Just copy for now
-	install -m 755 $(srcdir)/$(TARGET).py ./$(TARGET)
diff --git a/deployment.pri b/deployment.pri
new file mode 100644
index 0000000000000000000000000000000000000000..30f39b844554120225fee3e8c25b9c9c2d8ef43d
--- /dev/null
+++ b/deployment.pri
@@ -0,0 +1,21 @@
+unix:!android {
+    isEmpty(PREFIX) {
+        PREFIX = /usr/local
+    }
+
+    SRCDIR = src
+    BINDIR = $${PREFIX}/bin
+    DATADIR = $${PREFIX}/share
+
+    target.path = $${BINDIR}
+
+    desktop.files = $${SRCDIR}/desktop/$${TARGET}.desktop
+    desktop.path = $${DATADIR}/applications
+
+    icon.files = $${SRCDIR}/desktop/$${TARGET}.svg
+    icon.path = $${DATADIR}/icons/hicolor/scalable/apps
+
+    INSTALLS += target desktop icon
+}
+
+export(INSTALLS)
diff --git a/pkg/arch/PKGBUILD b/pkg/arch/PKGBUILD
index 64cd7ff4467d045026c3930c9b661dc6a3d8b806..03bce6c9071c40d0f474dea634407d4a6234ff58 100644
--- a/pkg/arch/PKGBUILD
+++ b/pkg/arch/PKGBUILD
@@ -7,7 +7,7 @@ pkgdesc="An install helper program for desktop stuff."
 arch=('i686' 'x86_64')
 url="https://github.com/xdgurl/xdgurl"
 license=('GPL3')
-depends=('tk')
+depends=('qt5-base>=5.3.0' 'qt5-declarative>=5.3.0' 'qt5-svg>=5.3.0' 'qt5-quickcontrols>=5.3.0')
 #source=("https://github.com/xdgurl/xdgurl/archive/release-$pkgver.tar.gz")
 source=("$pkgname.tar.gz")
 md5sums=() #autofill using updpkgsums
@@ -15,11 +15,12 @@ md5sums=() #autofill using updpkgsums
 build() {
     #cd "$pkgname-release-$pkgver"
     cd $pkgname
+    qmake PREFIX="/usr"
     make
 }
 
 package() {
     #cd "$pkgname-release-$pkgver"
     cd $pkgname
-    make DESTDIR="$pkgdir" prefix="/usr" install
+    make INSTALL_ROOT="$pkgdir" install
 }
diff --git a/pkg/build.sh b/pkg/build.sh
index 6e6782b838c6da56fc7f0042c3df495ac4f2d32c..97f5d699ef099aa9c6212e5f5795580e2a898a39 100644
--- a/pkg/build.sh
+++ b/pkg/build.sh
@@ -5,7 +5,8 @@ cd `dirname $0`
 build_ubuntu() {
     mkdir ./build
     cp -r ../src ./build/
-    cp ../Makefile ./build/
+    cp ../*.pro ./build/
+    cp ../*.pri ./build/
     cp -r ./ubuntu/debian ./build/
     cd ./build
     debuild -uc -us -b
diff --git a/pkg/fedora/xdgurl.spec b/pkg/fedora/xdgurl.spec
index 6efd32c9c1fb05cbc943dc5502527347024b8062..f986afd12f7e196ee233aa2144af85369dc9c57f 100644
--- a/pkg/fedora/xdgurl.spec
+++ b/pkg/fedora/xdgurl.spec
@@ -9,7 +9,7 @@ URL: https://github.com/xdgurl/xdgurl
 #Source0: https://github.com/xdgurl/xdgurl/archive/release-%{version}.tar.gz
 Source0: %{name}.tar.gz
 
-Requires: tkinter, python3-tkinter
+Requires: qt5-qtbase >= 5.3.0, qt5-qtsvg >= 5.3.0, qt5-qtdeclarative >= 5.3.0, qt5-qtquickcontrols >= 5.3.0
 
 %description
 An install helper program for desktop stuff.
@@ -20,10 +20,11 @@ An install helper program for desktop stuff.
 
 %build
 %define debug_package %{nil}
+qmake PREFIX="/usr"
 make
 
 %install
-make DESTDIR="%{buildroot}" prefix="/usr" install
+make INSTALL_ROOT="%{buildroot}" install
 
 %files
 %defattr(-,root,root)
diff --git a/pkg/ubuntu/debian/control b/pkg/ubuntu/debian/control
index 68a1d227a8200f93b03a4cdabd1a5422b5dc9bf1..e98ffa95b6a78afe5040a2c4428d91e019de3a99 100644
--- a/pkg/ubuntu/debian/control
+++ b/pkg/ubuntu/debian/control
@@ -2,11 +2,11 @@ Source: xdgurl
 Section: web
 Priority: optional
 Maintainer: Akira Ohgaki <akiraohgaki@gmail.com>
-Build-Depends: build-essential (>= 11), devscripts (>= 2.14), debhelper (>= 9), fakeroot (>= 1.20)
+Build-Depends: build-essential (>= 11), qt5-default (>= 5.3.0), qtdeclarative5-dev (>= 5.3.0), libqt5svg5-dev (>= 5.3.0), devscripts (>= 2.14), debhelper (>= 9), fakeroot (>= 1.20)
 Standards-Version: 3.9.4
 
 Package: xdgurl
 Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, python-tk (>= 2.7.0), python3-tk (>= 3.2.0)
+Depends: ${shlibs:Depends}, ${misc:Depends}, libqt5svg5 (>= 5.3.0), qtdeclarative5-qtquick2-plugin (>= 5.3.0), qtdeclarative5-window-plugin (>= 5.3.0), qtdeclarative5-controls-plugin (>= 5.3.0), qtdeclarative5-dialogs-plugin (>= 5.3.0)
 Description: xdgurl
  An install helper program for desktop stuff.
diff --git a/pkg/ubuntu/debian/rules b/pkg/ubuntu/debian/rules
index 999ca813027742773a158ff28651d583379299ac..c974694ac22c9622a4a93945ffd7400c36f0bae3 100755
--- a/pkg/ubuntu/debian/rules
+++ b/pkg/ubuntu/debian/rules
@@ -3,8 +3,8 @@
 %:
 	dh $@
 
-override_dh_auto_install:
-	make DESTDIR="$(CURDIR)/debian/tmp" prefix="/usr" install
+override_dh_auto_configure:
+	qmake PREFIX="/usr"
 
-override_dh_shlibdeps:
-	# ignore
+override_dh_auto_install:
+	make INSTALL_ROOT="$(CURDIR)/debian/tmp" install
diff --git a/rpath.pri b/rpath.pri
new file mode 100644
index 0000000000000000000000000000000000000000..b3cd65e83d8e1df1c1d7bc94254dc152a2a91226
--- /dev/null
+++ b/rpath.pri
@@ -0,0 +1,5 @@
+unix:!android {
+    QMAKE_LFLAGS += -Wl,-rpath=\\\$\$ORIGIN
+    QMAKE_LFLAGS += -Wl,-rpath=/usr/lib/$${TARGET}
+    QMAKE_RPATH =
+}
diff --git a/src/configs.qrc b/src/configs.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..4d5da4c70cb63857859e5a7b17e299989b48d182
--- /dev/null
+++ b/src/configs.qrc
@@ -0,0 +1,7 @@
+<RCC>
+    <qresource prefix="/">
+        <file>configs/application.json</file>
+        <file>configs/destinations.json</file>
+        <file>configs/destinations_alias.json</file>
+    </qresource>
+</RCC>
diff --git a/src/configs/application.json b/src/configs/application.json
new file mode 100644
index 0000000000000000000000000000000000000000..0b5e4aaf3f1d9553527921bd8d40353f65db3140
--- /dev/null
+++ b/src/configs/application.json
@@ -0,0 +1,13 @@
+{
+    "id": "xdgurl",
+    "name": "xdgurl",
+    "version": "1.1.0",
+    "organization": "xdgurl",
+    "domain": "com.xdgurl.xdgurl",
+    "icon": ":/desktop/xdgurl.svg",
+    "description": "An install helper program for desktop stuff.",
+    "license": "GPL-3+",
+    "author": "Akira Ohgaki",
+    "contact": "akiraohgaki@gmail.com",
+    "homepage": "https://github.com/xdgurl/xdgurl"
+}
diff --git a/src/configs/destinations.json b/src/configs/destinations.json
new file mode 100644
index 0000000000000000000000000000000000000000..65e2c3e3e2b22ed99a268c112699100da37e4ccc
--- /dev/null
+++ b/src/configs/destinations.json
@@ -0,0 +1,37 @@
+{
+    "downloads": "$HOME/Downloads",
+    "documents": "$HOME/Documents",
+    "pictures": "$HOME/Pictures",
+    "music": "$HOME/Music",
+    "videos": "$HOME/Videos",
+    "wallpapers": "$XDG_DATA_HOME/wallpapers",
+    "fonts": "$HOME/.fonts",
+    "cursors": "$HOME/.icons",
+    "icons": "$XDG_DATA_HOME/icons",
+    "emoticons": "$XDG_DATA_HOME/emoticons",
+    "themes": "$HOME/.themes",
+    "emerald_themes": "$HOME/.emerald/themes",
+    "enlightenment_themes": "$HOME/.e/e/themes",
+    "enlightenment_backgrounds": "$HOME/.e/e/backgrounds",
+    "fluxbox_styles": "$HOME/.fluxbox/styles",
+    "pekwm_themes": "$HOME/.pekwm/themes",
+    "icewm_themes": "$HOME/.icewm/themes",
+    "plasma_plasmoids": "$XDG_DATA_HOME/plasma/plasmoids",
+    "plasma_look_and_feel": "$XDG_DATA_HOME/plasma/look-and-feel",
+    "plasma_desktopthemes": "$XDG_DATA_HOME/plasma/desktoptheme",
+    "kwin_effects": "$XDG_DATA_HOME/kwin/effects",
+    "kwin_scripts": "$XDG_DATA_HOME/kwin/scripts",
+    "kwin_tabbox": "$XDG_DATA_HOME/kwin/tabbox",
+    "aurorae_themes": "$XDG_DATA_HOME/aurorae/themes",
+    "dekorator_themes": "$XDG_DATA_HOME/deKorator/themes",
+    "qtcurve": "$XDG_DATA_HOME/QtCurve",
+    "color_schemes": "$XDG_DATA_HOME/color-schemes",
+    "gnome_shell_extensions": "$XDG_DATA_HOME/gnome-shell/extensions",
+    "cinnamon_applets": "$XDG_DATA_HOME/cinnamon/applets",
+    "cinnamon_desklets": "$XDG_DATA_HOME/cinnamon/desklets",
+    "cinnamon_extensions": "$XDG_DATA_HOME/cinnamon/extensions",
+    "nautilus_scripts": "$XDG_DATA_HOME/nautilus/scripts",
+    "amarok_scripts": "$KDEHOME/share/apps/amarok/scripts",
+    "yakuake_skins": "$KDEHOME/share/apps/yakuake/skins",
+    "cairo_clock_themes": "$HOME/.cairo-clock/themes"
+}
diff --git a/src/configs/destinations_alias.json b/src/configs/destinations_alias.json
new file mode 100644
index 0000000000000000000000000000000000000000..438fb4dc92cf31716b7741064141b8a10a83f20d
--- /dev/null
+++ b/src/configs/destinations_alias.json
@@ -0,0 +1,17 @@
+{
+    "gnome_shell_themes": "themes",
+    "cinnamon_themes": "themes",
+    "gtk2_themes": "themes",
+    "gtk3_themes": "themes",
+    "metacity_themes": "themes",
+    "xfwm4_themes": "themes",
+    "openbox_themes": "themes",
+    "kvantum_themes": "themes",
+    "compiz_themes": "emerald_themes",
+    "beryl_themes": "emerald_themes",
+    "plasma4_plasmoids": "plasma_plasmoids",
+    "plasma5_plasmoids": "plasma_plasmoids",
+    "plasma5_look_and_feel": "plasma_look_and_feel",
+    "plasma5_desktopthemes": "plasma_desktopthemes",
+    "plasma_color_schemes": "color_schemes"
+}
diff --git a/src/core/config.cpp b/src/core/config.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..49b3b99a7e918765e7957aebcd2307577bbbcb68
--- /dev/null
+++ b/src/core/config.cpp
@@ -0,0 +1,39 @@
+#include "../utility/file.h"
+#include "../utility/json.h"
+
+#include "config.h"
+
+namespace Core {
+
+Config::Config(const QString &configsDir, QObject *parent) :
+    QObject(parent), _configsDir(configsDir)
+{}
+
+QJsonObject Config::get(const QString &name)
+{
+    QString configFile = _configsDir + "/" + name + ".json";
+
+    if (!_cacheData.contains(name)) {
+        QString json = Utility::File::readText(configFile);
+        if (json.isEmpty()) {
+            json = "{}"; // Blank JSON data as default
+        }
+        _cacheData[name] = Utility::Json::convertStrToObj(json);
+    }
+    return _cacheData[name].toObject();
+}
+
+bool Config::set(const QString &name, const QJsonObject &jsonObj)
+{
+    QString configFile = _configsDir + "/" + name + ".json";
+    QString json = Utility::Json::convertObjToStr(jsonObj);
+
+    Utility::File::makeDir(_configsDir);
+    if (Utility::File::writeText(configFile, json)) {
+        _cacheData[name] = jsonObj;
+        return true;
+    }
+    return false;
+}
+
+} // namespace Core
diff --git a/src/core/config.h b/src/core/config.h
new file mode 100644
index 0000000000000000000000000000000000000000..1a4ae9a70ef43f566421155f2eceb80d4a34fe89
--- /dev/null
+++ b/src/core/config.h
@@ -0,0 +1,26 @@
+#ifndef CORE_CONFIG_H
+#define CORE_CONFIG_H
+
+#include <QObject>
+#include <QJsonObject>
+
+namespace Core {
+
+class Config : public QObject
+{
+    Q_OBJECT
+
+private:
+    QString _configsDir;
+    QJsonObject _cacheData;
+
+public:
+    explicit Config(const QString &configsDir, QObject *parent = 0);
+
+    QJsonObject get(const QString &name);
+    bool set(const QString &name, const QJsonObject &jsonObj);
+};
+
+} // namespace Core
+
+#endif // CORE_CONFIG_H
diff --git a/src/core/network.cpp b/src/core/network.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d040d2939477f7020befaa23b6ae50a58d19b28
--- /dev/null
+++ b/src/core/network.cpp
@@ -0,0 +1,65 @@
+#include <QEventLoop>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+
+#include "network.h"
+
+namespace Core {
+
+Network::Network(const bool &async, QObject *parent) :
+    QObject(parent), _async(async)
+{
+    _manager = new QNetworkAccessManager();
+    connect(_manager, &QNetworkAccessManager::finished,
+            this, &Network::_finished);
+    if (!_async) {
+        _eventLoop = new QEventLoop();
+        connect(_manager, &QNetworkAccessManager::finished,
+                _eventLoop, &QEventLoop::quit);
+    }
+}
+
+Network::~Network()
+{
+    _manager->deleteLater();
+    if (!_async) {
+        delete _eventLoop;
+    }
+}
+
+QNetworkReply *Network::head(const QUrl &uri)
+{
+    QNetworkReply *reply = _manager->head(QNetworkRequest(uri));
+    if (!_async) {
+        _eventLoop->exec();
+    }
+    return reply;
+}
+
+QNetworkReply *Network::get(const QUrl &uri)
+{
+    QNetworkReply *reply = _manager->get(QNetworkRequest(uri));
+    connect(reply, &QNetworkReply::downloadProgress,
+            this, &Network::_downloadProgress);
+    if (!_async) {
+        _eventLoop->exec();
+    }
+    return reply;
+}
+
+/**
+ * Private slots
+ */
+
+void Network::_finished(QNetworkReply *reply)
+{
+    emit finished(reply);
+}
+
+void Network::_downloadProgress(const qint64 &received, const qint64 &total)
+{
+    emit downloadProgress(received, total);
+}
+
+} // namespace Core
diff --git a/src/core/network.h b/src/core/network.h
new file mode 100644
index 0000000000000000000000000000000000000000..374310c5ec00eea27e3d0e06c81a8a94eaae132e
--- /dev/null
+++ b/src/core/network.h
@@ -0,0 +1,39 @@
+#ifndef CORE_NETWORK_H
+#define CORE_NETWORK_H
+
+#include <QObject>
+
+class QEventLoop;
+class QNetworkAccessManager;
+class QNetworkReply;
+
+namespace Core {
+
+class Network : public QObject
+{
+    Q_OBJECT
+
+private:
+    bool _async;
+    QNetworkAccessManager *_manager;
+    QEventLoop *_eventLoop;
+
+public:
+    explicit Network(const bool &async = true, QObject *parent = 0);
+    ~Network();
+
+    QNetworkReply *head(const QUrl &uri);
+    QNetworkReply *get(const QUrl &uri);
+
+private slots:
+    void _finished(QNetworkReply *reply);
+    void _downloadProgress(const qint64 &received, const qint64 &total);
+
+signals:
+    void finished(QNetworkReply *reply);
+    void downloadProgress(const qint64 &received, const qint64 &total);
+};
+
+} // namespace Core
+
+#endif // CORE_NETWORK_H
diff --git a/src/desktop.qrc b/src/desktop.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..929b2f8ae623c418161f652dceecb0c340ca2a2c
--- /dev/null
+++ b/src/desktop.qrc
@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="/">
+        <file>desktop/xdgurl.svg</file>
+    </qresource>
+</RCC>
diff --git a/src/handlers/xdgurl.cpp b/src/handlers/xdgurl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8391558febb0c7229035317fc256caf78f539a38
--- /dev/null
+++ b/src/handlers/xdgurl.cpp
@@ -0,0 +1,294 @@
+#include <QUrl>
+#include <QUrlQuery>
+#include <QTemporaryFile>
+#include <QMimeDatabase>
+#include <QNetworkReply>
+
+#include "../core/config.h"
+#include "../core/network.h"
+#include "../utility/file.h"
+#include "../utility/json.h"
+#include "../utility/package.h"
+
+#include "xdgurl.h"
+
+namespace Handlers {
+
+XdgUrl::XdgUrl(const QString &xdgUrl, Core::Config *config, Core::Network *network, QObject *parent) :
+    QObject(parent), _xdgUrl(xdgUrl), _config(config), _network(network)
+{
+    _parse();
+    _loadDestinations();
+
+    connect(_network, &Core::Network::finished, this, &XdgUrl::_downloaded);
+    connect(_network, &Core::Network::downloadProgress, this, &XdgUrl::downloadProgress);
+}
+
+void XdgUrl::_parse()
+{
+    QUrl url(_xdgUrl);
+    QUrlQuery query(url);
+
+    _metadata["scheme"] = QString("xdg");
+    _metadata["command"] = QString("download");
+    _metadata["url"] = QString("");
+    _metadata["type"] = QString("downloads");
+    _metadata["filename"] = QString("");
+
+    if (!url.scheme().isEmpty()) {
+        _metadata["scheme"] = url.scheme();
+    }
+
+    if (!url.host().isEmpty()) {
+        _metadata["command"] = url.host();
+    }
+
+    if (query.hasQueryItem("url") && !query.queryItemValue("url").isEmpty()) {
+        _metadata["url"] = query.queryItemValue("url", QUrl::FullyDecoded);
+    }
+
+    if (query.hasQueryItem("type") && !query.queryItemValue("type").isEmpty()) {
+        _metadata["type"] = query.queryItemValue("type", QUrl::FullyDecoded);
+    }
+
+    if (query.hasQueryItem("filename") && !query.queryItemValue("filename").isEmpty()) {
+        _metadata["filename"] = QUrl(query.queryItemValue("filename", QUrl::FullyDecoded)).fileName();
+    }
+
+    if (!_metadata["url"].toString().isEmpty() && _metadata["filename"].toString().isEmpty()) {
+        _metadata["filename"] = QUrl(_metadata["url"].toString()).fileName();
+    }
+}
+
+void XdgUrl::_loadDestinations()
+{
+    QJsonObject configDestinations = _config->get("destinations");
+    QJsonObject configDestinationsAlias = _config->get("destinations_alias");
+
+    foreach (const QString key, configDestinations.keys()) {
+        _destinations[key] = _convertPathString(configDestinations[key].toString());
+    }
+
+    foreach (const QString key, configDestinationsAlias.keys()) {
+        QString value = configDestinationsAlias[key].toString();
+        if (_destinations.contains(value)) {
+            _destinations[key] = _destinations.value(value);
+        }
+    }
+}
+
+QString XdgUrl::_convertPathString(const QString &path)
+{
+    QString newPath = path;
+
+    if (newPath.contains("$HOME")) {
+        newPath.replace("$HOME", Utility::File::homePath());
+    }
+    else if (newPath.contains("$XDG_DATA_HOME")) {
+        newPath.replace("$XDG_DATA_HOME", Utility::File::xdgDataHomePath());
+    }
+    else if (newPath.contains("$KDEHOME")) {
+        newPath.replace("$KDEHOME", Utility::File::kdehomePath());
+    }
+
+    return newPath;
+}
+
+void XdgUrl::_saveDownloadedFile(QNetworkReply *reply)
+{
+    QJsonObject result;
+
+    QTemporaryFile temporaryFile;
+
+    if (!temporaryFile.open() || temporaryFile.write(reply->readAll()) == -1) {
+        result["status"] = QString("error_save");
+        result["message"] = temporaryFile.errorString();
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    QMimeDatabase mimeDb;
+    QString mimeType = mimeDb.mimeTypeForFile(temporaryFile.fileName()).name();
+
+    if (mimeType == "text/html" || mimeType == "application/xhtml+xml") {
+        result["status"] = QString("error_filetype");
+        result["message"] = QString("The file is unsupported file type " + mimeType);
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    QString type = _metadata["type"].toString();
+    QString destination = _destinations[type].toString();
+    QString path = destination + "/" + _metadata["filename"].toString();
+
+    Utility::File::makeDir(destination);
+    Utility::File::remove(path); // Remove previous downloaded file
+
+    if (!temporaryFile.copy(path)) {
+        result["status"] = QString("error_save");
+        result["message"] = temporaryFile.errorString();
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    result["status"] = QString("success_download");
+    result["message"] = QString("The file has been stored into " + destination);
+    emit finished(Utility::Json::convertObjToStr(result));
+}
+
+void XdgUrl::_installDownloadedFile(QNetworkReply *reply)
+{
+    QJsonObject result;
+
+    QTemporaryFile temporaryFile;
+
+    if (!temporaryFile.open() || temporaryFile.write(reply->readAll()) == -1) {
+        result["status"] = QString("error_save");
+        result["message"] = temporaryFile.errorString();
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    QMimeDatabase mimeDb;
+    QString mimeType = mimeDb.mimeTypeForFile(temporaryFile.fileName()).name();
+
+    if (mimeType == "text/html" || mimeType == "application/xhtml+xml") {
+        result["status"] = QString("error_filetype");
+        result["message"] = QString("The file is unsupported file type " + mimeType);
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    QString type = _metadata["type"].toString();
+    QString destination = _destinations[type].toString();
+    QString path = destination + "/" + _metadata["filename"].toString();
+
+    Utility::File::makeDir(destination);
+    Utility::File::remove(path); // Remove previous downloaded file
+
+    if ((type == "plasma_plasmoids" || type == "plasma4_plasmoids" || type == "plasma5_plasmoids")
+            && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "plasmoid")) {
+        result["message"] = QString("The plasmoid has been installed");
+    }
+    else if ((type == "plasma_look_and_feel" || type == "plasma5_look_and_feel")
+             && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "lookandfeel")) {
+        result["message"] = QString("The plasma look and feel has been installed");
+    }
+    else if ((type == "plasma_desktopthemes" || type == "plasma5_desktopthemes")
+             && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "theme")) {
+        result["message"] = QString("The plasma desktop theme has been installed");
+    }
+    else if (type == "kwin_effects"
+             && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "kwineffect")) {
+        result["message"] = QString("The KWin effect has been installed");
+    }
+    else if (type == "kwin_scripts"
+             && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "kwinscript")) {
+        result["message"] = QString("The KWin script has been installed");
+    }
+    else if (type == "kwin_tabbox"
+             && Utility::Package::installPlasmapkg(temporaryFile.fileName(), "windowswitcher")) {
+        result["message"] = QString("The KWin window switcher has been installed");
+    }
+    else if (Utility::Package::uncompressArchive(temporaryFile.fileName(), destination)) {
+        result["message"] = QString("The archive file has been uncompressed into " + destination);
+    }
+    else if (temporaryFile.copy(path)) {
+        result["message"] = QString("The file has been stored into " + destination);
+    }
+    else {
+        result["status"] = QString("error_install");
+        result["message"] = temporaryFile.errorString();
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    result["status"] = QString("success_install");
+    emit finished(Utility::Json::convertObjToStr(result));
+}
+
+/**
+ * Private slots
+ */
+
+void XdgUrl::_downloaded(QNetworkReply *reply)
+{
+    if (reply->error() != QNetworkReply::NoError) {
+        QJsonObject result;
+        result["status"] = QString("error_network");
+        result["message"] = reply->errorString();
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    // If the network reply has a refresh header, retry download
+    if (reply->hasRawHeader("Refresh")) {
+        QString refreshUrl = QString(reply->rawHeader("Refresh")).split("url=").last();
+        if (refreshUrl.startsWith("/")) {
+            refreshUrl = reply->url().authority() + refreshUrl;
+        }
+        _network->get(QUrl(refreshUrl));
+        return;
+    }
+
+    if (_metadata["command"].toString() == "download") {
+        _saveDownloadedFile(reply);
+    }
+    else if (_metadata["command"].toString() == "install") {
+        _installDownloadedFile(reply);
+    }
+}
+
+/**
+ * Public slots
+ */
+
+QString XdgUrl::getXdgUrl()
+{
+    return _xdgUrl;
+}
+
+QString XdgUrl::getMetadata()
+{
+    return Utility::Json::convertObjToStr(_metadata);
+}
+
+bool XdgUrl::isValid()
+{
+    QString scheme = _metadata["scheme"].toString();
+    QString command = _metadata["command"].toString();
+    QString url = _metadata["url"].toString();
+    QString type = _metadata["type"].toString();
+    QString filename = _metadata["filename"].toString();
+
+    if ((scheme == "xdg" || scheme == "xdgs")
+            && (command == "download" || command == "install")
+            && QUrl(url).isValid()
+            && _destinations.contains(type)
+            && !filename.isEmpty()) {
+        return true;
+    }
+
+    return false;
+}
+
+void XdgUrl::process()
+{
+    /**
+     * xdgs scheme is a reserved name, so the process of xdgs
+     * is the same process of the xdg scheme currently.
+     */
+
+    if (!isValid()) {
+        QJsonObject result;
+        result["status"] = QString("error_validation");
+        result["message"] = QString("Invalid XDG-URL " + _xdgUrl);
+        emit error(Utility::Json::convertObjToStr(result));
+        return;
+    }
+
+    _network->get(QUrl(_metadata["url"].toString()));
+    emit started();
+}
+
+} // namespace Handlers
diff --git a/src/handlers/xdgurl.h b/src/handlers/xdgurl.h
new file mode 100644
index 0000000000000000000000000000000000000000..becb66906f6a9c0a0a93cda79390ac5db35b67a9
--- /dev/null
+++ b/src/handlers/xdgurl.h
@@ -0,0 +1,56 @@
+#ifndef HANDLERS_XDGURL_H
+#define HANDLERS_XDGURL_H
+
+#include <QObject>
+#include <QJsonObject>
+
+class QNetworkReply;
+
+namespace Core {
+class Config;
+class Network;
+}
+
+namespace Handlers {
+
+class XdgUrl : public QObject
+{
+    Q_OBJECT
+
+private:
+    QString _xdgUrl;
+    Core::Config *_config;
+    Core::Network *_network;
+
+    QJsonObject _metadata;
+    QJsonObject _destinations;
+
+public:
+    explicit XdgUrl(const QString &xdgUrl, Core::Config *config, Core::Network *network, QObject *parent = 0);
+
+private:
+    void _parse();
+    void _loadDestinations();
+    QString _convertPathString(const QString &path);
+    void _saveDownloadedFile(QNetworkReply *reply);
+    void _installDownloadedFile(QNetworkReply *reply);
+
+private slots:
+    void _downloaded(QNetworkReply *reply);
+
+public slots:
+    QString getXdgUrl();
+    QString getMetadata();
+    bool isValid();
+    void process();
+
+signals:
+    void started();
+    void finished(const QString &result);
+    void error(const QString &result);
+    void downloadProgress(const qint64 &received, const qint64 &total);
+};
+
+} // namespace Handlers
+
+#endif // HANDLERS_XDGURL_H
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..64f2d6ef3d2e56c3916c60fc0087f9c0b31ca791
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,55 @@
+#include <QString>
+#include <QStringList>
+#include <QUrl>
+#include <QJsonObject>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QGuiApplication>
+#include <QIcon>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+
+#include "core/config.h"
+#include "core/network.h"
+#include "handlers/xdgurl.h"
+
+int main(int argc, char *argv[])
+{
+    // Init
+    //QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // Qt 5.6 or higher
+    QGuiApplication app(argc, argv);
+    Core::Config *config = new Core::Config(":/configs");
+    Core::Network *network = new Core::Network(true);
+
+    QJsonObject configApplication = config->get("application");
+
+    app.setApplicationName(configApplication["name"].toString());
+    app.setApplicationVersion(configApplication["version"].toString());
+    app.setOrganizationName(configApplication["organization"].toString());
+    app.setOrganizationDomain(configApplication["domain"].toString());
+    app.setWindowIcon(QIcon::fromTheme(configApplication["id"].toString(), QIcon(configApplication["icon"].toString())));
+
+    // Setup CLI
+    QCommandLineParser clParser;
+    clParser.setApplicationDescription(configApplication["description"].toString());
+    clParser.addHelpOption();
+    clParser.addVersionOption();
+    clParser.addPositionalArgument("xdgurl", "XDG-URL");
+    clParser.process(app);
+
+    QStringList args = clParser.positionalArguments();
+
+    if (args.size() != 1) {
+        clParser.showHelp(1);
+    }
+
+    QString xdgUrl = args.at(0);
+
+    // Setup QML
+    QQmlApplicationEngine qmlAppEngine;
+    QQmlContext *qmlContext = qmlAppEngine.rootContext();
+    qmlContext->setContextProperty("xdgUrlHandler", new Handlers::XdgUrl(xdgUrl, config, network));
+    qmlAppEngine.load(QUrl("qrc:/qml/main.qml"));
+
+    return app.exec();
+}
diff --git a/src/qml.qrc b/src/qml.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..69145a822f1f6a3a133b38da279dab003922da12
--- /dev/null
+++ b/src/qml.qrc
@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/main.qml</file>
+    </qresource>
+</RCC>
diff --git a/src/qml/main.qml b/src/qml/main.qml
new file mode 100644
index 0000000000000000000000000000000000000000..6327dacb43d8b44a63aa64d52a6f62e631860370
--- /dev/null
+++ b/src/qml/main.qml
@@ -0,0 +1,194 @@
+import QtQuick 2.0
+import QtQuick.Window 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Dialogs 1.2
+
+Window {
+    id: root
+    title: Qt.application.name
+    width: 400
+    height: 200
+    minimumWidth: 400
+    minimumHeight: 200
+    maximumWidth: 800
+    maximumHeight: 400
+
+    MessageDialog {
+        id: confirmDialog
+        title: root.title
+        icon: StandardIcon.Question
+        text: ''
+        informativeText: ''
+        detailedText: ''
+        standardButtons: StandardButton.Ok | StandardButton.Cancel
+        onAccepted: xdgUrlHandler.process()
+        onRejected: Qt.quit()
+    }
+
+    MessageDialog {
+        id: infoDialog
+        title: root.title
+        icon: StandardIcon.Information
+        text: ''
+        informativeText: ''
+        detailedText: ''
+        standardButtons: StandardButton.Ok
+        onAccepted: Qt.quit()
+    }
+
+    MessageDialog {
+        id: errorDialog
+        title: root.title
+        icon: StandardIcon.Warning
+        text: ''
+        informativeText: ''
+        detailedText: ''
+        standardButtons: StandardButton.Ok
+        onAccepted: Qt.quit()
+    }
+
+    Dialog {
+        id: progressDialog
+        title: root.title
+        contentItem: Item {
+            implicitWidth: 400
+            implicitHeight: 150
+            Column {
+                anchors.fill: parent
+                anchors.margins: 12
+                spacing: 8
+                Label {
+                    id: primaryLabel
+                    text: ''
+                    font.bold: true
+                }
+                Label {
+                    id: informativeLabel
+                    text: ''
+                }
+                ProgressBar {
+                    id: progressBar
+                    maximumValue: 1
+                    minimumValue: 0
+                    value: 0
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                }
+                Label {
+                    id: progressLabel
+                    text: ''
+                    anchors.right: parent.right
+                }
+                Button {
+                    id: cancelButton
+                    text: 'Cancel'
+                    anchors.right: parent.right
+                    onClicked: Qt.quit()
+                }
+            }
+        }
+        property alias primaryLabel: primaryLabel
+        property alias informativeLabel: informativeLabel
+        property alias progressBar: progressBar
+        property alias progressLabel: progressLabel
+    }
+
+    Component.onCompleted: {
+        var metadata = JSON.parse(xdgUrlHandler.getMetadata());
+        var primaryMessages = {
+            'success_download': 'Download successfull',
+            'success_install': 'Installation successfull',
+            'error_validation': 'Validation error',
+            'error_network': 'Network error',
+            'error_filetype': 'File type error',
+            'error_save': 'Saving file failed',
+            'error_install': 'Installation failed'
+        };
+
+        xdgUrlHandler.started.connect(function() {
+            progressDialog.open();
+        });
+
+        xdgUrlHandler.finished.connect(function(result) {
+            progressDialog.close();
+            result = JSON.parse(result);
+            infoDialog.text = primaryMessages[result.status];
+            infoDialog.informativeText = metadata.filename;
+            infoDialog.detailedText = result.message;
+            infoDialog.open();
+        });
+
+        xdgUrlHandler.error.connect(function(result) {
+            progressDialog.close();
+            result = JSON.parse(result);
+            errorDialog.text = primaryMessages[result.status];
+            errorDialog.informativeText = metadata.filename;
+            errorDialog.detailedText = result.message;
+            errorDialog.open();
+        });
+
+        xdgUrlHandler.downloadProgress.connect(function(received, total) {
+            progressDialog.primaryLabel.text = 'Downloading... ';
+            progressDialog.informativeLabel.text = metadata.filename;
+            progressDialog.progressBar.value = received / total;
+            progressDialog.progressLabel.text = convertByteToHumanReadable(received)
+                    + ' / ' + convertByteToHumanReadable(total)
+        });
+
+        if (xdgUrlHandler.isValid()) {
+            confirmDialog.text = 'Do you want to ' + metadata.command + '?';
+            confirmDialog.informativeText = metadata.filename;
+            confirmDialog.detailedText = 'URL: ' + metadata.url + '\n\n'
+                    + 'File: ' + metadata.filename + '\n\n'
+                    + 'Type: ' + metadata.type;
+            confirmDialog.open();
+        }
+        else {
+            errorDialog.text = 'Validation error';
+            errorDialog.detailedText = 'Invalid XDG-URL ' + xdgUrlHandler.getXdgUrl();
+            errorDialog.open();
+        }
+    }
+
+    function convertByteToHumanReadable(bytes) {
+        bytes = parseFloat(bytes);
+        var kb = 1024;
+        var mb = 1024 * kb;
+        var gb = 1024 * mb;
+        var tb = 1024 * gb;
+        var pb = 1024 * tb;
+        var eb = 1024 * pb;
+        var zb = 1024 * eb;
+        var yb = 1024 * zb;
+
+        var text = '';
+        if (bytes < kb) {
+            text = bytes.toFixed(0) + ' B';
+        }
+        else if (bytes < mb) {
+            text = (bytes / kb).toFixed(2) + ' KB';
+        }
+        else if (bytes < gb) {
+            text = (bytes / mb).toFixed(2) + ' MB';
+        }
+        else if (bytes < tb) {
+            text = (bytes / gb).toFixed(2) + ' GB';
+        }
+        else if (bytes < pb) {
+            text = (bytes / tb).toFixed(2) + ' TB';
+        }
+        else if (bytes < eb) {
+            text = (bytes / pb).toFixed(2) + ' PB';
+        }
+        else if (bytes < zb) {
+            text = (bytes / eb).toFixed(2) + ' EB';
+        }
+        else if (bytes < yb) {
+            text = (bytes / zb).toFixed(2) + ' ZB';
+        }
+        else if (bytes >= yb) {
+            text = (bytes / yb).toFixed(2) + ' YB';
+        }
+        return text;
+    }
+}
diff --git a/src/utility/file.cpp b/src/utility/file.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2330ba180e2bffe3779b1a2912631364fdf91eb4
--- /dev/null
+++ b/src/utility/file.cpp
@@ -0,0 +1,208 @@
+#include <QIODevice>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+
+#include "file.h"
+
+namespace Utility {
+
+File::File(QObject *parent) : QObject(parent)
+{}
+
+QString File::rootPath()
+{
+    return QDir::rootPath();
+}
+
+QString File::tempPath()
+{
+    return QDir::tempPath();
+}
+
+QString File::homePath()
+{
+    return QDir::homePath();
+}
+
+/**
+ * XDG Base Directory Specification
+ * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+ */
+
+QString File::xdgDataHomePath()
+{
+    QString path = QString::fromLocal8Bit(qgetenv("XDG_DATA_HOME").constData());
+    if (path.isEmpty()) {
+        path = homePath() + "/.local/share";
+    }
+    return path;
+}
+
+QString File::xdgConfigHomePath()
+{
+    QString path = QString::fromLocal8Bit(qgetenv("XDG_CONFIG_HOME").constData());
+    if (path.isEmpty()) {
+        path = homePath() + "/.config";
+    }
+    return path;
+}
+
+QString File::xdgCacheHomePath()
+{
+    QString path = QString::fromLocal8Bit(qgetenv("XDG_CACHE_HOME").constData());
+    if (path.isEmpty()) {
+        path = homePath() + "/.cache";
+    }
+    return path;
+}
+
+/**
+ * KDE System Administration/Environment Variables
+ * https://userbase.kde.org/KDE_System_Administration/Environment_Variables
+ */
+
+QString File::kdehomePath()
+{
+    // KDE 4 maybe uses $KDEHOME
+    QString path = QString::fromLocal8Bit(qgetenv("KDEHOME").constData());
+    if (path.isEmpty()) {
+        path = homePath() + "/.kde";
+    }
+    return path;
+}
+
+QFileInfoList File::readDir(const QString &path)
+{
+    QDir dir(path);
+    dir.setFilter(QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
+    //dir.setSorting(QDir::DirsFirst | QDir::Name);
+    return dir.entryInfoList();
+}
+
+bool File::makeDir(const QString &path)
+{
+    // This function will create all parent directories
+    QDir dir(path);
+    if (!dir.exists() && dir.mkpath(path)) {
+        return true;
+    }
+    return false;
+}
+
+QString File::readText(const QString &path)
+{
+    QString data;
+    QFile file(path);
+    if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+        QTextStream in(&file);
+        in.setCodec("UTF-8");
+        data = in.readAll();
+        file.close();
+    }
+    return data;
+}
+
+bool File::writeText(const QString &path, const QString &data)
+{
+    QFile file(path);
+    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+        QTextStream out(&file);
+        out.setCodec("UTF-8");
+        out << data;
+        file.close();
+        return true;
+    }
+    return false;
+}
+
+QByteArray File::readBinary(const QString &path)
+{
+    QByteArray data;
+    QFile file(path);
+    if (file.exists() && file.open(QIODevice::ReadOnly)) {
+        data = file.readAll();
+        file.close();
+    }
+    return data;
+}
+
+bool File::writeBinary(const QString &path, const QByteArray &data)
+{
+    QFile file(path);
+    if (file.open(QIODevice::WriteOnly)) {
+        file.write(data);
+        file.close();
+        return true;
+    }
+    return false;
+}
+
+bool File::copy(const QString &path, const QString &targetPath)
+{
+    // This function will copy files recursively
+    QFileInfo fileInfo(path);
+    if (fileInfo.isFile()) {
+        QFile file(path);
+        if (file.copy(targetPath)) {
+            return true;
+        }
+    }
+    else if (fileInfo.isDir()) {
+        QDir targetDir(targetPath);
+        QString targetDirName = targetDir.dirName();
+        targetDir.cdUp();
+        if (targetDir.mkdir(targetDirName)) {
+            QDir dir(path);
+            dir.setFilter(QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
+            QStringList entries = dir.entryList();
+            foreach (const QString &entry, entries) {
+                if (!copy(path + "/" + entry, targetPath + "/" + entry)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+    return false;
+}
+
+bool File::move(const QString &path, const QString &targetPath)
+{
+    QFileInfo fileInfo(path);
+    if (fileInfo.isFile()) {
+        QFile file(path);
+        if (file.rename(targetPath)) {
+            return true;
+        }
+    }
+    else if (fileInfo.isDir()) {
+        QDir dir(path);
+        if (dir.rename(path, targetPath)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool File::remove(const QString &path)
+{
+    // This function will remove files recursively
+    QFileInfo fileInfo(path);
+    if (fileInfo.isFile()) {
+        QFile file(path);
+        if (file.remove()) {
+            return true;
+        }
+    }
+    else if (fileInfo.isDir()) {
+        QDir dir(path);
+        if (dir.removeRecursively()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+} // namespace Utility
diff --git a/src/utility/file.h b/src/utility/file.h
new file mode 100644
index 0000000000000000000000000000000000000000..8700250686283ff77ccc33fbb4001295c6c0ea1e
--- /dev/null
+++ b/src/utility/file.h
@@ -0,0 +1,38 @@
+#ifndef UTILITY_FILE_H
+#define UTILITY_FILE_H
+
+#include <QObject>
+
+class QFileInfo;
+typedef QList<QFileInfo> QFileInfoList;
+
+namespace Utility {
+
+class File : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit File(QObject *parent = 0);
+
+    static QString rootPath();
+    static QString tempPath();
+    static QString homePath();
+    static QString xdgDataHomePath();
+    static QString xdgConfigHomePath();
+    static QString xdgCacheHomePath();
+    static QString kdehomePath();
+    static QFileInfoList readDir(const QString &path);
+    static bool makeDir(const QString &path);
+    static QString readText(const QString &path);
+    static bool writeText(const QString &path, const QString &data);
+    static QByteArray readBinary(const QString &path);
+    static bool writeBinary(const QString &path, const QByteArray &data);
+    static bool copy(const QString &path, const QString &targetPath);
+    static bool move(const QString &path, const QString &targetPath);
+    static bool remove(const QString &path);
+};
+
+} // namespace Utility
+
+#endif // UTILITY_FILE_H
diff --git a/src/utility/json.cpp b/src/utility/json.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cbce058ca4917f8c073f5976657984f4ed71b0c0
--- /dev/null
+++ b/src/utility/json.cpp
@@ -0,0 +1,39 @@
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+
+#include "json.h"
+
+namespace Utility {
+
+Json::Json(QObject *parent) : QObject(parent)
+{}
+
+bool Json::isValid(const QString &json)
+{
+    QJsonParseError jsonError;
+    QJsonDocument::fromJson(json.toUtf8(), &jsonError);
+    if (jsonError.error == QJsonParseError::NoError) {
+        return true;
+    }
+    return false;
+}
+
+QJsonObject Json::convertStrToObj(const QString &json)
+{
+    QJsonObject jsonObj;
+    QJsonParseError jsonError;
+    QJsonDocument jsonDoc = QJsonDocument::fromJson(json.toUtf8(), &jsonError);
+    if (jsonError.error == QJsonParseError::NoError && jsonDoc.isObject()) {
+        jsonObj = jsonDoc.object();
+    }
+    return jsonObj;
+}
+
+QString Json::convertObjToStr(const QJsonObject &jsonObj)
+{
+    QJsonDocument jsonDoc(jsonObj);
+    return QString::fromUtf8(jsonDoc.toJson());
+}
+
+} // namespace Utility
diff --git a/src/utility/json.h b/src/utility/json.h
new file mode 100644
index 0000000000000000000000000000000000000000..68f100b58f25af7e48fc63999445976665e4c1d8
--- /dev/null
+++ b/src/utility/json.h
@@ -0,0 +1,22 @@
+#ifndef UTILITY_JSON_H
+#define UTILITY_JSON_H
+
+#include <QObject>
+
+namespace Utility {
+
+class Json : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit Json(QObject *parent = 0);
+
+    static bool isValid(const QString &json);
+    static QJsonObject convertStrToObj(const QString &json);
+    static QString convertObjToStr(const QJsonObject &jsonObj);
+};
+
+} // namespace Utility
+
+#endif // UTILITY_JSON_H
diff --git a/src/utility/package.cpp b/src/utility/package.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b3c5c75d9b6239e24160845fb3f75de4a4cd8192
--- /dev/null
+++ b/src/utility/package.cpp
@@ -0,0 +1,104 @@
+#include <QJsonObject>
+#include <QMimeDatabase>
+#include <QProcess>
+
+#include "package.h"
+
+namespace Utility {
+
+Package::Package(QObject *parent) : QObject(parent)
+{}
+
+bool Package::uncompressArchive(const QString &path, const QString &targetDir)
+{
+    QJsonObject archiveTypes;
+    archiveTypes["application/x-tar"] = QString("tar");
+    archiveTypes["application/x-gzip"] = QString("tar");
+    archiveTypes["application/gzip"] = QString("tar");
+    archiveTypes["application/x-bzip"] = QString("tar");
+    archiveTypes["application/x-bzip2"] = QString("tar");
+    archiveTypes["application/x-xz"] = QString("tar");
+    archiveTypes["application/x-lzma"] = QString("tar");
+    archiveTypes["application/x-lzip"] = QString("tar");
+    archiveTypes["application/x-compressed-tar"] = QString("tar");
+    archiveTypes["application/x-bzip-compressed-tar"] = QString("tar");
+    archiveTypes["application/x-bzip2-compressed-tar"] = QString("tar");
+    archiveTypes["application/x-xz-compressed-tar"] = QString("tar");
+    archiveTypes["application/x-lzma-compressed-tar"] = QString("tar");
+    archiveTypes["application/x-lzip-compressed-tar"] = QString("tar");
+    archiveTypes["application/zip"] = QString("zip");
+    archiveTypes["application/x-7z-compressed"] = QString("7z");
+    archiveTypes["application/x-rar"] = QString("rar");
+    archiveTypes["application/x-rar-compressed"] = QString("rar");
+
+    QMimeDatabase mimeDb;
+    QString mimeType = mimeDb.mimeTypeForFile(path).name();
+
+    if (archiveTypes.contains(mimeType)) {
+        QString archiveType = archiveTypes[mimeType].toString();
+
+        QProcess process;
+        QString program;
+        QStringList arguments;
+
+        if (archiveType == "tar") {
+            program = "tar";
+            arguments << "-xf" << path << "-C" << targetDir;
+        }
+        else if (archiveType == "zip") {
+            program = "unzip";
+            arguments << "-o" << path << "-d" << targetDir;
+        }
+        else if (archiveType == "7z") {
+            program = "7z";
+            arguments << "x" << path << "-o" + targetDir; // No space between -o and directory
+        }
+        else if (archiveType == "rar") {
+            program = "unrar";
+            arguments << "e" << path << targetDir;
+        }
+
+        process.start(program, arguments);
+
+        if (process.waitForFinished()) {
+            process.waitForReadyRead();
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool Package::installPlasmapkg(const QString &path, const QString &type)
+{
+    QProcess process;
+    QString program = "plasmapkg2"; // Use plasmapkg2 for now
+    QStringList arguments;
+    arguments << "-t" << type << "-i" << path;
+
+    process.start(program, arguments);
+
+    if (process.waitForFinished()) {
+        return true;
+    }
+
+    return false;
+}
+
+bool Package::uninstallPlasmapkg(const QString &path, const QString &type)
+{
+    QProcess process;
+    QString program = "plasmapkg2"; // Use plasmapkg2 for now
+    QStringList arguments;
+    arguments << "-t" << type << "-r" << path;
+
+    process.start(program, arguments);
+
+    if (process.waitForFinished()) {
+        return true;
+    }
+
+    return false;
+}
+
+} // namespace Utility
diff --git a/src/utility/package.h b/src/utility/package.h
new file mode 100644
index 0000000000000000000000000000000000000000..89bf0375dc5b1e02298a467a27f5cb7e857b0f12
--- /dev/null
+++ b/src/utility/package.h
@@ -0,0 +1,22 @@
+#ifndef UTILITY_PACKAGE_H
+#define UTILITY_PACKAGE_H
+
+#include <QObject>
+
+namespace Utility {
+
+class Package : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit Package(QObject *parent = 0);
+
+    static bool uncompressArchive(const QString &path, const QString &targetDir);
+    static bool installPlasmapkg(const QString &path, const QString &type = "plasmoid");
+    static bool uninstallPlasmapkg(const QString &path, const QString &type = "plasmoid");
+};
+
+} // namespace Utility
+
+#endif // UTILITY_PACKAGE_H
diff --git a/src/xdgurl.py b/src/xdgurl.py
deleted file mode 100644
index 11ae5085dca88a767b79fce38c2f09fea4fd872c..0000000000000000000000000000000000000000
--- a/src/xdgurl.py
+++ /dev/null
@@ -1,377 +0,0 @@
-#!/usr/bin/env python
-
-"""xdgurl
-An install helper program for desktop stuff.
-
-Copyright: 2016, Akira Ohgaki
-License: GPL-3+
-
-https://github.com/xdgurl/xdgurl
-"""
-
-import sys
-import os
-import shutil
-import json
-import tempfile
-import mimetypes
-import subprocess
-import argparse
-
-if sys.version_info.major >= 3:
-    import urllib.request
-    import urllib.error
-    import urllib.parse
-    import tkinter
-    import tkinter.messagebox
-else:
-    import urllib
-    import urlparse
-    import Tkinter
-    import tkMessageBox
-
-class XdgUrl:
-
-    def __init__(self, xdg_url=''):
-        self.xdg_url = xdg_url
-        self.meta = self.parse()
-
-        self.temp_dir = tempfile.gettempdir()
-        self.home_dir = os.path.expanduser('~')
-        #self.config_dir = os.path.join(self.home_dir, '.config', 'xdgurl')
-        self.data_dir = os.path.join(self.home_dir, '.local', 'share')
-        self.kde_data_dir = os.path.join(self.home_dir, '.kde', 'share')
-
-        #self.config = {}
-
-        self.destinations = {
-            'downloads': os.path.join(self.home_dir, 'Downloads'),
-            'documents': os.path.join(self.home_dir, 'Documents'),
-            'pictures': os.path.join(self.home_dir, 'Pictures'),
-            'music': os.path.join(self.home_dir, 'Music'),
-            'videos': os.path.join(self.home_dir, 'Videos'),
-            'wallpapers': os.path.join(self.data_dir, 'wallpapers'),
-            'fonts': os.path.join(self.home_dir, '.fonts'),
-            'cursors': os.path.join(self.home_dir, '.icons'),
-            'icons': os.path.join(self.data_dir, 'icons'),
-            'emoticons': os.path.join(self.data_dir, 'emoticons'),
-            'themes': os.path.join(self.home_dir, '.themes'),
-            'emerald_themes': os.path.join(self.home_dir, '.emerald', 'themes'),
-            'enlightenment_themes': os.path.join(self.home_dir, '.e', 'e', 'themes'),
-            'enlightenment_backgrounds': os.path.join(self.home_dir, '.e', 'e', 'backgrounds'),
-            'fluxbox_styles': os.path.join(self.home_dir, '.fluxbox', 'styles'),
-            'pekwm_themes': os.path.join(self.home_dir, '.pekwm', 'themes'),
-            'icewm_themes': os.path.join(self.home_dir, '.icewm', 'themes'),
-            'plasma_plasmoids': os.path.join(self.data_dir, 'plasma', 'plasmoids'),
-            'plasma_look_and_feel': os.path.join(self.data_dir, 'plasma', 'look-and-feel'),
-            'plasma_desktopthemes': os.path.join(self.data_dir, 'plasma', 'desktoptheme'),
-            'kwin_effects': os.path.join(self.data_dir, 'kwin', 'effects'),
-            'kwin_scripts': os.path.join(self.data_dir, 'kwin', 'scripts'),
-            'kwin_tabbox': os.path.join(self.data_dir, 'kwin', 'tabbox'),
-            'aurorae_themes': os.path.join(self.data_dir, 'aurorae', 'themes'),
-            'dekorator_themes': os.path.join(self.data_dir, 'deKorator', 'themes'),
-            'qtcurve': os.path.join(self.data_dir, 'QtCurve'),
-            'color_schemes': os.path.join(self.data_dir, 'color-schemes'),
-            'gnome_shell_extensions': os.path.join(self.data_dir, 'gnome-shell', 'extensions'),
-            'cinnamon_applets': os.path.join(self.data_dir, 'cinnamon', 'applets'),
-            'cinnamon_desklets': os.path.join(self.data_dir, 'cinnamon', 'desklets'),
-            'cinnamon_extensions': os.path.join(self.data_dir, 'cinnamon', 'extensions'),
-            'nautilus_scripts': os.path.join(self.data_dir, 'nautilus', 'scripts'),
-            'amarok_scripts': os.path.join(self.kde_data_dir, 'apps', 'amarok', 'scripts'),
-            'yakuake_skins': os.path.join(self.kde_data_dir, 'apps', 'yakuake', 'skins'),
-            'cairo_clock_themes': os.path.join(self.home_dir, '.cairo-clock', 'themes')
-        }
-        self.destinations_alias = {
-            'gnome_shell_themes': self.destinations['themes'],
-            'cinnamon_themes': self.destinations['themes'],
-            'gtk2_themes': self.destinations['themes'],
-            'gtk3_themes': self.destinations['themes'],
-            'metacity_themes': self.destinations['themes'],
-            'xfwm4_themes': self.destinations['themes'],
-            'openbox_themes': self.destinations['themes'],
-            'kvantum_themes': self.destinations['themes'],
-            'compiz_themes': self.destinations['emerald_themes'],
-            'beryl_themes': self.destinations['emerald_themes'],
-            'plasma4_plasmoids': self.destinations['plasma_plasmoids'],
-            'plasma5_plasmoids': self.destinations['plasma_plasmoids'],
-            'plasma5_look_and_feel': self.destinations['plasma_look_and_feel'],
-            'plasma5_desktopthemes': self.destinations['plasma_desktopthemes'],
-            'plasma_color_schemes': self.destinations['color_schemes']
-        }
-        self.destinations.update(self.destinations_alias)
-
-        self.archive_types = {
-            'tar': [
-                'application/x-tar',
-                'application/x-gzip',
-                'application/gzip',
-                'application/x-bzip',
-                'application/x-bzip2',
-                'application/x-xz',
-                'application/x-lzma',
-                'application/x-lzip',
-                'application/x-compressed-tar',
-                'application/x-bzip-compressed-tar',
-                'application/x-bzip2-compressed-tar',
-                'application/x-xz-compressed-tar',
-                'application/x-lzma-compressed-tar',
-                'application/x-lzip-compressed-tar'
-            ],
-            'zip': ['application/zip'],
-            '7z': ['application/x-7z-compressed'],
-            'rar': [
-                'application/x-rar',
-                'application/x-rar-compressed'
-            ]
-        }
-
-        #self.config.update(self.read_config('config'))
-        #self.destinations.update(self.read_config('destinations'))
-
-    def read_config(self, name):
-        path = os.path.join(self.config_dir, name + '.json')
-        data = {}
-        if os.path.isfile(path):
-            f = open(path, 'r')
-            data = json.load(f)
-            f.close()
-        return data
-
-    def write_config(self, name, data):
-        if not os.path.isdir(self.config_dir):
-            os.makedirs(self.config_dir)
-        path = os.path.join(self.config_dir, name + '.json')
-        f = open(path, 'w')
-        json.dump(data, f)
-        f.close()
-
-    def parse(self):
-        meta = {
-            'command': 'download',
-            'url': '',
-            'type': 'downloads',
-            'filename': ''
-        }
-
-        if sys.version_info.major >= 3:
-            parse_result = urllib.parse.urlparse(self.xdg_url)
-            query = urllib.parse.parse_qs(parse_result.query)
-        else:
-            parse_result = urlparse.urlparse(self.xdg_url)
-            query = urlparse.parse_qs(parse_result.query)
-
-        if parse_result.netloc:
-            meta['command'] = parse_result.netloc
-
-        if 'url' in query and query['url'][0]:
-            if sys.version_info.major >= 3:
-                meta['url'] = urllib.parse.unquote(query['url'][0])
-            else:
-                meta['url'] = urllib.unquote(query['url'][0])
-
-        if 'type' in query and query['type'][0]:
-            if sys.version_info.major >= 3:
-                meta['type'] = urllib.parse.unquote(query['type'][0])
-            else:
-                meta['type'] = urllib.unquote(query['type'][0])
-
-        if 'filename' in query and query['filename'][0]:
-            if sys.version_info.major >= 3:
-                meta['filename'] = urllib.parse.unquote(query['filename'][0]).split('?')[0]
-            else:
-                meta['filename'] = urllib.unquote(query['filename'][0]).split('?')[0]
-
-        if meta['url'] and not meta['filename']:
-            meta['filename'] = os.path.basename(meta['url']).split('?')[0]
-
-        return meta
-
-    """
-    def detect_desktop_environment(self):
-        desktop_environment = 'unknown'
-        if os.environ.get('KDE_FULL_SESSION') == 'true':
-            desktop_environment = 'kde'
-        elif os.environ.get('GNOME_DESKTOP_SESSION_ID'):
-            desktop_environment = 'gnome'
-        return desktop_environment
-    """
-
-    def install_plasmapkg(self, path, type='plasmoid'):
-        status = subprocess.call(['plasmapkg2', '-t', type, '-i', path])
-        if status == 0:
-            return True
-        return False
-
-    def uncompress_archive(self, path, target_dir):
-        (mimetype, encoding) = mimetypes.guess_type(path)
-        status = None
-
-        if mimetype in self.archive_types['tar']:
-            status = subprocess.call(['tar', '-xf', path, '-C', target_dir])
-        elif mimetype in self.archive_types['zip']:
-            status = subprocess.call(['unzip', '-o', path, '-d', target_dir])
-        elif mimetype in self.archive_types['7z']:
-            # No space between -o and directory
-            status = subprocess.call(['7z', 'x', path, '-o' + target_dir])
-        elif mimetype in self.archive_types['rar']:
-            status = subprocess.call(['unrar', 'e', path, target_dir])
-
-        if status == 0:
-            return True
-        return False
-
-    def download(self):
-        url = self.meta['url']
-        type = self.meta['type']
-        filename = self.meta['filename']
-        destination = self.destinations[type]
-
-        temp_path = os.path.join(self.temp_dir, filename)
-        path = os.path.join(destination, filename)
-
-        print('Retrieving a file from ' + url)
-        if sys.version_info.major >= 3:
-            urllib.request.urlretrieve(url, temp_path)
-        else:
-            urllib.urlretrieve(url, temp_path)
-
-        print('Creating a directory ' + destination)
-        if not os.path.isdir(destination):
-            os.makedirs(destination)
-
-        print('Saving a file to ' + path)
-        shutil.move(temp_path, path)
-
-        print('Done')
-
-    def install(self):
-        url = self.meta['url']
-        type = self.meta['type']
-        filename = self.meta['filename']
-        destination = self.destinations[type]
-
-        temp_path = os.path.join(self.temp_dir, filename)
-        path = os.path.join(destination, filename)
-
-        print('Retrieving a file from ' + url)
-        if sys.version_info.major >= 3:
-            urllib.request.urlretrieve(url, temp_path)
-        else:
-            urllib.urlretrieve(url, temp_path)
-
-        print('Creating a directory ' + destination)
-        if not os.path.isdir(destination):
-            os.makedirs(destination)
-
-        print('Installing')
-        if (type in ['plasma_plasmoids', 'plasma4_plasmoids', 'plasma5_plasmoids']
-        and self.install_plasmapkg(temp_path, 'plasmoid')):
-            print('The plasmoid has been installed')
-            os.remove(temp_path)
-        elif (type in ['plasma_look_and_feel', 'plasma5_look_and_feel']
-        and self.install_plasmapkg(temp_path, 'lookandfeel')):
-            print('The plasma look and feel has been installed')
-            os.remove(temp_path)
-        elif (type in ['plasma_desktopthemes', 'plasma5_desktopthemes']
-        and self.install_plasmapkg(temp_path, 'theme')):
-            print('The plasma desktop theme has been installed')
-            os.remove(temp_path)
-        elif (type == 'kwin_effects'
-        and self.install_plasmapkg(temp_path, 'kwineffect')):
-            print('The KWin effect has been installed')
-            os.remove(temp_path)
-        elif (type == 'kwin_scripts'
-        and self.install_plasmapkg(temp_path, 'kwinscript')):
-            print('The KWin script has been installed')
-            os.remove(temp_path)
-        elif (type == 'kwin_tabbox'
-        and self.install_plasmapkg(temp_path, 'windowswitcher')):
-            print('The KWin window switcher has been installed')
-            os.remove(temp_path)
-        elif self.uncompress_archive(temp_path, destination):
-            print('The archive file has been uncompressed into ' + destination)
-            os.remove(temp_path)
-        else:
-            print('Saving a file to ' + path)
-            shutil.move(temp_path, path)
-
-        print('Done')
-
-    def execute(self):
-        if (self.meta['command'] in ['download', 'install']
-        and self.meta['url']
-        and self.meta['type'] in self.destinations
-        and self.meta['filename']):
-            if self.meta['command'] == 'download':
-                self.download()
-            elif self.meta['command'] == 'install':
-                self.install()
-        else:
-            raise Exception('Incorrect XDG-URL ' + self.xdg_url)
-
-def main():
-    program = 'xdgurl'
-    version = '1.0.1'
-
-    parser = argparse.ArgumentParser(
-        prog=program,
-        description='An install helper program for desktop stuff',
-        epilog='Check more information on https://github.com/xdgurl/xdgurl'
-    )
-    parser.add_argument(
-        '-v', '--version',
-        action='version',
-        version='%(prog)s ' + version
-    )
-    parser.add_argument('xdg_url', help='XDG-URL')
-    args = parser.parse_args()
-
-    if args.xdg_url:
-        core = XdgUrl(args.xdg_url)
-        if sys.version_info.major >= 3:
-            window = tkinter.Tk()
-        else:
-            window = Tkinter.Tk()
-        window.withdraw()
-
-        info_text = 'Download: ' + core.meta['filename'] + '\nFrom: ' + core.meta['url']
-        if core.meta['command'] == 'install':
-            info_text = 'Install: ' + core.meta['filename'] + '\nFrom: ' + core.meta['url']
-
-        message = 'Do you want to continue?'
-
-        print(info_text)
-        if sys.version_info.major >= 3:
-            confirm = tkinter.messagebox.askyesno(program, info_text + '\n\n' + message)
-        else:
-            confirm = tkMessageBox.askyesno(program, info_text + '\n\n' + message)
-
-        if confirm:
-            try:
-                core.execute();
-            except Exception as e:
-                message = 'Download failed'
-                if core.meta['command'] == 'install':
-                    message = 'Installation failed'
-
-                print(message)
-                if sys.version_info.major >= 3:
-                    tkinter.messagebox.showerror(program, info_text + '\n\n' + message + '\n' + str(e))
-                else:
-                    tkMessageBox.showerror(program, info_text + '\n\n' + message + '\n' + str(e))
-
-                return str(e) # stderr and exit code 1
-            else:
-                message = 'Download successfull'
-                if core.meta['command'] == 'install':
-                    message = 'Installation successfull'
-
-                print(message)
-                if sys.version_info.major >= 3:
-                    tkinter.messagebox.showinfo(program, message)
-                else:
-                    tkMessageBox.showinfo(program, message)
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
diff --git a/xdgurl.pro b/xdgurl.pro
new file mode 100644
index 0000000000000000000000000000000000000000..096220bc3c93904c291df132aa0093bee7260030
--- /dev/null
+++ b/xdgurl.pro
@@ -0,0 +1,46 @@
+TARGET = xdgurl
+
+TEMPLATE = app
+
+QT += \
+    core \
+    gui \
+    widgets \
+    qml \
+    quick \
+    svg \
+    network
+
+CONFIG += \
+    c++11
+
+SOURCES += \
+    src/main.cpp \
+    src/core/config.cpp \
+    src/core/network.cpp \
+    src/handlers/xdgurl.cpp \
+    src/utility/file.cpp \
+    src/utility/json.cpp \
+    src/utility/package.cpp
+
+HEADERS += \
+    src/core/config.h \
+    src/core/network.h \
+    src/handlers/xdgurl.h \
+    src/utility/file.h \
+    src/utility/json.h \
+    src/utility/package.h
+
+RESOURCES += \
+    src/configs.qrc \
+    src/qml.qrc \
+    src/desktop.qrc
+
+DISTFILES += \
+    README.md
+
+# Additional RPATH
+#include(rpath.pri)
+
+# Deployment rules
+include(deployment.pri)