A => .gitignore +79 -0
@@ 1,79 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+build-dir
+cmake-build-debug
+
+# qtcreator generated files
+*.pro.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
+# Flatpak
+.flatpak-builder/
+
+.idea
A => LICENSE +20 -0
@@ 1,20 @@
+Copyright 2020 Tristan Lins
+Copyright 2021 Nova <novaburst@tilde.team>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
A => README.md +4 -0
@@ 1,4 @@
+# io.kimiko.qtwebapp
+This is a ThreemaQT based project for use in desktop web applications without the need for Electron / node.js
+
+Licensed under the [MIT License](./COPYING) , just like the original project.
A => desktop/share/applications/io.kimiko.qtwebapp.desktop +7 -0
@@ 1,7 @@
+[Desktop Entry]
+Name=%%APPLICATION%%
+Icon=%%APPLICATION%%
+Exec=%%APPLICATION%%
+Description=QtWebEngine-powered desktop client for %%APPLICATION%%
+Categories=Network;
+Type=Application
A => desktop/share/icons/hicolor/scalable/apps/io.kimiko.qtwebapp.png +0 -0
A => qtwebapp.pro +49 -0
@@ 1,49 @@
+QT += core widgets webengine webenginewidgets
+
+CONFIG += c++11 console
+
+# The following define makes your compiler emit warnings if you use
+# any Qt feature that has been marked deprecated (the exact warnings
+# depend on your compiler). Refer to the documentation for the
+# deprecated API to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ src/main.cpp \
+ src/mainwindow.cpp \
+ src/singletonwebenginepage.cpp
+
+RESOURCES += \
+ qtwebapp.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH =
+
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
+
+
+DISTFILES += \
+ LICENSE \
+
+HEADERS += \
+ src/mainwindow.h \
+ src/singletonwebenginepage.h
+
+isEmpty(PREFIX):PREFIX = /usr/local
+
+target.path = $$PREFIX/bin/
+INSTALLS += target
+
+icons.path = $$PREFIX/share/icons
+icons.files += desktop/share/icons/*
+INSTALLS += icons
+
+desktop.path = $$PREFIX/share/applications
+desktop.files += desktop/share/applications/*
+INSTALLS += desktop
A => qtwebapp.qrc +5 -0
@@ 1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>images/bloat.png</file>
+ </qresource>
+</RCC>
A => src/main.cpp +26 -0
@@ 1,26 @@
+#include <QApplication>
+#include <QtWidgets>
+#include <QtWebEngine>
+#include <QWebEngineProfile>
+
+#include "mainwindow.h"
+
+int main(int argc, char *argv[])
+{
+ QtWebEngine::initialize();
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ QApplication app(argc, argv);
+ QCoreApplication::setOrganizationName("");
+ QCoreApplication::setApplicationName("");
+ QCoreApplication::setApplicationVersion(QT_VERSION_STR);
+ app.setWindowIcon(QIcon(""));
+
+ QWebEngineProfile *profile = QWebEngineProfile::defaultProfile();
+ profile->setHttpUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36");
+
+ MainWindow window;
+ window.show();
+
+ return app.exec();
+}
A => src/mainwindow.cpp +229 -0
@@ 1,229 @@
+#include "mainwindow.h"
+#include "singletonwebenginepage.h"
+#include <QApplication>
+#include <QAction>
+#include <QSettings>
+#include <QWebEngineProfile>
+#include <QFileDialog>
+#include <QMessageBox>
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent),
+ notificationsTitleRegExp("^\\([1-9]\\d*\\).*"),
+ trayIconRead("/images/.png"),
+ trayIconUnread("/images/.png")
+{
+ setWindowTitle("");
+ setWindowIcon(QIcon(""));
+ setMinimumWidth(800);
+ setMinimumHeight(600);
+ readSettings();
+
+ createActions();
+ createStatusBar();
+ createTrayIcon();
+ createWebEngine();
+
+ // Connect signals and slots
+ connect(webEngine, &QWebEngineView::titleChanged,
+ this, &MainWindow::handleWebViewTitleChanged);
+ connect(webEngine, &QWebEngineView::loadStarted,
+ this, &MainWindow::handleLoadStarted);
+ connect(webEngine, &QWebEngineView::loadProgress,
+ this, &MainWindow::handleLoadProgress);
+ connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested,
+ this, &MainWindow::handleDownloadRequested);
+}
+
+void MainWindow::readSettings()
+{
+ QSettings settings("io.kimiko", "");
+ restoreGeometry(settings.value("main/geometry").toByteArray());
+ restoreState(settings.value("main/windowState").toByteArray());
+}
+
+void MainWindow::showAbout()
+{
+ QString applicationVersion = QCoreApplication::applicationVersion();
+ QMessageBox about(this);
+ about.setWindowTitle(tr(""));
+ about.setIconPixmap(QPixmap(""));
+ about.setTextFormat(Qt::TextFormat::RichText);
+ about.addButton(QMessageBox::StandardButton::Ok);
+ about.exec();
+}
+
+void MainWindow::showAboutQt()
+{
+ QMessageBox::aboutQt(this, tr(""));
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+ QSettings settings("io.kimiko", "");
+ settings.setValue("main/geometry", saveGeometry());
+ settings.setValue("main/windowState", saveState());
+ QMainWindow::closeEvent(event);
+}
+
+void MainWindow::createActions()
+{
+ reloadAction = new QAction(tr("Re&load"), this);
+ reloadAction->setShortcut(Qt::Key_F5);
+ connect(reloadAction, &QAction::triggered, this, &MainWindow::doReload);
+ addAction(reloadAction);
+
+ minimizeAction = new QAction(tr("Mi&nimize"), this);
+ connect(minimizeAction, &QAction::triggered, this, &QWidget::hide);
+ addAction(minimizeAction);
+
+ maximizeAction = new QAction(tr("Ma&ximize"), this);
+ connect(maximizeAction, &QAction::triggered, this, &QWidget::showMaximized);
+ addAction(maximizeAction);
+
+ restoreAction = new QAction(tr("&Restore"), this);
+ connect(restoreAction, &QAction::triggered, this, &QWidget::showNormal);
+ addAction(restoreAction);
+
+ aboutAction = new QAction(tr("About"), this);
+ connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout);
+
+ aboutAction = new QAction(tr("About"), this);
+ connect(aboutAction, &QAction::triggered, this, &MainWindow::showAbout);
+
+ aboutQtAction = new QAction(tr("About QT"), this);
+ connect(aboutQtAction, &QAction::triggered, this, &MainWindow::showAboutQt);
+
+ quitAction = new QAction(tr("&Quit"), this);
+ quitAction->setShortcut(QKeySequence(Qt::Modifier::CTRL + Qt::Key_Q));
+ connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit);
+ addAction(quitAction);
+}
+
+void MainWindow::createStatusBar()
+{
+ QStatusBar *statusBar = new QStatusBar(this);
+ setStatusBar(statusBar);
+ statusBar->hide();
+ this->statusBar = statusBar;
+
+ QProgressBar *progressBar = new QProgressBar(this->statusBar);
+ statusBar->addWidget(progressBar, 1);
+ progressBar->setTextVisible(false);
+ progressBar->hide();
+ this->progressBar = progressBar;
+}
+
+void MainWindow::createTrayIcon()
+{
+ trayIconMenu = new QMenu(this);
+ trayIconMenu->addAction(reloadAction);
+ trayIconMenu->addSeparator();
+ trayIconMenu->addAction(minimizeAction);
+ trayIconMenu->addAction(maximizeAction);
+ trayIconMenu->addAction(restoreAction);
+ trayIconMenu->addSeparator();
+ trayIconMenu->addAction(aboutAction);
+ trayIconMenu->addAction(aboutQtAction);
+ trayIconMenu->addSeparator();
+ trayIconMenu->addAction(quitAction);
+
+ trayIcon = new QSystemTrayIcon(trayIconRead, this);
+ trayIcon->setContextMenu(trayIconMenu);
+
+ trayIcon->show();
+
+ connect(trayIcon, &QSystemTrayIcon::messageClicked,
+ this, &MainWindow::messageClicked);
+ connect(trayIcon, &QSystemTrayIcon::activated,
+ this, &MainWindow::iconActivated);
+}
+
+void MainWindow::createWebEngine()
+{
+ QSizePolicy widgetSize;
+ widgetSize.setHorizontalPolicy(QSizePolicy::Expanding);
+ widgetSize.setVerticalPolicy(QSizePolicy::Expanding);
+ widgetSize.setHorizontalStretch(1);
+ widgetSize.setVerticalStretch(1);
+
+ QWebEngineView *webEngine = new QWebEngineView(this);
+ setCentralWidget(webEngine);
+ webEngine->setSizePolicy(widgetSize);
+ webEngine->show();
+ this->webEngine = webEngine;
+
+ QWebEnginePage *page = new SingletonWebEnginePage(webEngine);
+ webEngine->setPage(page);
+ page->setUrl(QUrl(""));
+}
+
+void MainWindow::handleWebViewTitleChanged(QString title) {
+ setWindowTitle(title);
+
+ if (notificationsTitleRegExp.exactMatch(title))
+ {
+ trayIcon->setIcon(trayIconUnread);
+ }
+ else
+ {
+ trayIcon->setIcon(trayIconRead);
+ }
+}
+
+void MainWindow::handleLoadStarted()
+{
+ statusBar->show();
+ progressBar->show();
+}
+
+void MainWindow::handleLoadProgress(int progress)
+{
+ if (progress >= 100)
+ {
+ progressBar->hide();
+ statusBar->hide();
+ }
+ else
+ {
+ progressBar->setValue(progress);
+ }
+}
+
+void MainWindow::handleDownloadRequested(QWebEngineDownloadItem *download)
+{
+ QFileDialog dialog;
+ dialog.setAcceptMode(QFileDialog::AcceptMode::AcceptSave);
+ dialog.setFileMode(QFileDialog::FileMode::AnyFile);
+ dialog.selectFile(download->suggestedFileName());
+
+ if (dialog.exec() && dialog.selectedFiles().size() > 0)
+ {
+ download->setDownloadDirectory(dialog.directory().absolutePath());
+ download->setDownloadFileName(dialog.selectedFiles().at(0));
+ download->accept();
+ }
+}
+
+void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
+{
+ if (isVisible()) {
+ hide();
+ } else {
+ showNormal();
+ }
+}
+
+void MainWindow::messageClicked()
+{
+ if (isVisible()) {
+ hide();
+ } else {
+ showNormal();
+ }
+}
+
+void MainWindow::doReload()
+{
+ this->webEngine->triggerPageAction(QWebEnginePage::ReloadAndBypassCache, false);
+}
A => src/mainwindow.h +64 -0
@@ 1,64 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QProgressBar>
+#include <QStatusBar>
+#include <QWebEngineView>
+#include <QIcon>
+#include <QMenu>
+#include <QSystemTrayIcon>
+#include <QRegExp>
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+public:
+ explicit MainWindow(QWidget *parent = nullptr);
+
+protected:
+ void closeEvent(QCloseEvent *event) override;
+
+private:
+ void createActions();
+ void createStatusBar();
+ void createTrayIcon();
+ void createWebEngine();
+
+ QRegExp notificationsTitleRegExp;
+ QIcon trayIconRead;
+ QIcon trayIconUnread;
+
+ QAction *reloadAction;
+ QAction *minimizeAction;
+ QAction *maximizeAction;
+ QAction *restoreAction;
+ QAction *aboutAction;
+ QAction *aboutQtAction;
+ QAction *quitAction;
+
+ QMenu *trayIconMenu;
+ QSystemTrayIcon *trayIcon;
+
+ QWebEngineView *webEngine;
+ QStatusBar *statusBar;
+ QProgressBar *progressBar;
+ void reload();
+ void readSettings();
+
+public slots:
+ void handleWebViewTitleChanged(QString title);
+ void handleLoadStarted();
+ void handleLoadProgress(int progress);
+ void handleDownloadRequested(QWebEngineDownloadItem *download);
+ void iconActivated(QSystemTrayIcon::ActivationReason reason);
+ void messageClicked();
+ void doReload();
+ void showAbout();
+ void showAboutQt();
+
+signals:
+
+};
+
+#endif // MAINWINDOW_H
A => src/singletonwebenginepage.cpp +52 -0
@@ 1,52 @@
+#include "singletonwebenginepage.h"
+
+#include <QDesktopServices>
+
+SingletonWebEnginePage::SingletonWebEnginePage(QWidget *parent)
+ : QWebEnginePage(parent)
+{
+ // Connect signals and slots
+ connect(this, &QWebEnginePage::featurePermissionRequested,
+ this, &SingletonWebEnginePage::handleFeaturePermissionRequested);
+ connect(this, &QWebEnginePage::loadFinished,
+ this, &SingletonWebEnginePage::handleLoadFinished);
+}
+
+bool SingletonWebEnginePage::acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame)
+{
+ qDebug() << "Navigation request: [" + url.toDisplayString() + "] " + type;
+
+ if (QWebEnginePage::NavigationType::NavigationTypeLinkClicked == type)
+ {
+ QDesktopServices::openUrl(url);
+ return false;
+ }
+
+ return QWebEnginePage::acceptNavigationRequest(url, type, isMainFrame);
+}
+
+QWebEnginePage *SingletonWebEnginePage::createWindow(QWebEnginePage::WebWindowType type)
+{
+ return new SingletonWebEnginePage();
+}
+
+void SingletonWebEnginePage::handleFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature)
+{
+ if (QWebEnginePage::Feature::Notifications == feature)
+ {
+ setFeaturePermission(
+ securityOrigin,
+ feature,
+ QWebEnginePage::PermissionPolicy::PermissionGrantedByUser
+ );
+ }
+}
+
+void SingletonWebEnginePage::handleLoadFinished(bool ok)
+{
+ setFeaturePermission(
+ QUrl("https://web.threema.ch/"),
+ QWebEnginePage::Feature::Notifications,
+ QWebEnginePage::PermissionPolicy::PermissionGrantedByUser
+ );
+}
A => src/singletonwebenginepage.h +23 -0
@@ 1,23 @@
+#ifndef SINGLETONWEBENGINEPAGE_H
+#define SINGLETONWEBENGINEPAGE_H
+
+#include <QWebEnginePage>
+
+
+
+class SingletonWebEnginePage : public QWebEnginePage
+{
+ Q_OBJECT
+public:
+ SingletonWebEnginePage(QWidget *parent = nullptr);
+
+protected:
+ bool acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame) override;
+ QWebEnginePage* createWindow(QWebEnginePage::WebWindowType type) override;
+
+public slots:
+ void handleFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature);
+ void handleLoadFinished(bool ok);
+};
+
+#endif // SINGLETONWEBENGINEPAGE_H