[neon/neon-packaging/plasma-bigscreen/Neon/unstable] debian/patches: patch is already in
Carlos De Maine
null at kde.org
Sun Aug 3 14:31:58 BST 2025
Git commit ff56bdb2ed74ded820dd00fd9908bc02eff61541 by Carlos De Maine.
Committed on 03/08/2025 at 13:31.
Pushed by carlosdem into branch 'Neon/unstable'.
patch is already in
D +0 -1 debian/patches/series
D +0 -2037 debian/patches/work_devinlin_webviewer.diff
https://invent.kde.org/neon/neon-packaging/plasma-bigscreen/-/commit/ff56bdb2ed74ded820dd00fd9908bc02eff61541
diff --git a/debian/patches/series b/debian/patches/series
deleted file mode 100644
index ba6302f..0000000
--- a/debian/patches/series
+++ /dev/null
@@ -1 +0,0 @@
-work_devinlin_webviewer.diff
diff --git a/debian/patches/work_devinlin_webviewer.diff b/debian/patches/work_devinlin_webviewer.diff
deleted file mode 100644
index ed22d48..0000000
--- a/debian/patches/work_devinlin_webviewer.diff
+++ /dev/null
@@ -1,2037 +0,0 @@
-diff --git a/CMakeLists.txt b/CMakeLists.txt
-index 899dcd6..7575aaf 100644
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -10,6 +10,7 @@ set(PROJECT_VERSION "6.4.80")
-
- set(QT_MIN_VERSION "6.8.0")
- set(KF_MIN_VERSION "6.14.0")
-+set(QCORO_MIN_VERSION "0.7.0")
-
- set(CMAKE_CXX_STANDARD 20)
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
-@@ -62,7 +63,14 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
- DBus
- Network
- Multimedia
-+ WebEngineCore
-+ WebEngineQuick
- )
-+find_package(QCoro6 ${QCORO_MIN_VERSION} REQUIRED COMPONENTS Core Quick Qml)
-+
-+add_definitions(-DQT_NO_FOREACH -DQT_NO_URL_CAST_FROM_STRING)
-+kde_enable_exceptions()
-+qcoro_enable_coroutines()
-
- ecm_find_qmlmodule(org.kde.plasma.core 2.0)
-
-@@ -77,6 +85,7 @@ add_subdirectory(components)
- add_subdirectory(sounds)
- add_subdirectory(envmanager)
- add_subdirectory(uvcviewer)
-+add_subdirectory(webapp-viewer)
-
- plasma_install_package(shell org.kde.plasma.bigscreen shells)
- plasma_install_package(lookandfeel org.kde.plasma.bigscreen look-and-feel lookandfeel)
-diff --git a/README.md b/README.md
-index e723c28..65b7ce5 100644
---- a/README.md
-+++ b/README.md
-@@ -65,6 +65,10 @@ See [this page](https://community.kde.org/Get_Involved/development) in order to
- - DBus
- - Network
-
-+### Other dependencies
-+
-+- QCoro
-+
- </details>
-
- To start the Bigscreen homescreen in a window, use the following script:
-diff --git a/components/bigscreenplugin/qml/AbstractDelegate.qml b/components/bigscreenplugin/qml/AbstractDelegate.qml
-index ee3108a..8b3c0fc 100644
---- a/components/bigscreenplugin/qml/AbstractDelegate.qml
-+++ b/components/bigscreenplugin/qml/AbstractDelegate.qml
-@@ -39,8 +39,10 @@ QQC2.ItemDelegate {
- z: isCurrent ? 2 : 0
-
- onClicked: {
-- listView.forceActiveFocus()
-- listView.currentIndex = index
-+ if (listView) {
-+ listView.forceActiveFocus()
-+ listView.currentIndex = index
-+ }
- }
-
- leftPadding: Kirigami.Units.gridUnit
-diff --git a/kcms/CMakeLists.txt b/kcms/CMakeLists.txt
-index 32d4360..a639b61 100644
---- a/kcms/CMakeLists.txt
-+++ b/kcms/CMakeLists.txt
-@@ -6,4 +6,5 @@ add_subdirectory(audio-device-chooser)
- add_subdirectory(wifi)
- add_subdirectory(kdeconnect)
- add_subdirectory(bigscreen-settings)
--add_subdirectory(display)
-\ No newline at end of file
-+add_subdirectory(display)
-+add_subdirectory(webapps)
-\ No newline at end of file
-diff --git a/kcms/webapps/CMakeLists.txt b/kcms/webapps/CMakeLists.txt
-new file mode 100644
-index 0000000..75d761e
---- /dev/null
-+++ b/kcms/webapps/CMakeLists.txt
-@@ -0,0 +1,27 @@
-+# SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+# SPDX-License-Identifier: GPL-2.0-or-later
-+
-+set(webappskcm_SRCS
-+ webappskcm.cpp
-+ webappcreator.cpp
-+ webappmanager.cpp
-+ webappmanagermodel.cpp
-+)
-+
-+kcmutils_add_qml_kcm(kcm_mediacenter_webapps SOURCES ${webappskcm_SRCS})
-+
-+target_link_libraries(kcm_mediacenter_webapps
-+ Qt::DBus
-+ Qt::Gui
-+ Qt::Quick
-+ Qt::Qml
-+ QCoro6::Qml
-+ KF6::ConfigCore
-+ KF6::Svg
-+ KF6::I18n
-+ KF6::KCMUtilsQuick
-+ KF6::ConfigWidgets
-+ KF6::CoreAddons
-+ KF6::Package
-+ Plasma::Plasma
-+)
-diff --git a/kcms/webapps/kcm_mediacenter_webapps.json b/kcms/webapps/kcm_mediacenter_webapps.json
-new file mode 100644
-index 0000000..9b354c2
---- /dev/null
-+++ b/kcms/webapps/kcm_mediacenter_webapps.json
-@@ -0,0 +1,12 @@
-+{
-+ "KPlugin": {
-+ "Description": "Create and manage web applications",
-+ "FormFactors": [
-+ "mediacenter"
-+ ],
-+ "Icon": "internet-web-browser-symbolic",
-+ "Name": "Web Apps"
-+ },
-+ "X-KDE-System-Settings-Parent-Category": "appearance",
-+ "X-KDE-Weight": 40
-+}
-diff --git a/kcms/webapps/ui/WebAppInfoSidebar.qml b/kcms/webapps/ui/WebAppInfoSidebar.qml
-new file mode 100644
-index 0000000..27f414c
---- /dev/null
-+++ b/kcms/webapps/ui/WebAppInfoSidebar.qml
-@@ -0,0 +1,100 @@
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org
-+// SPDX-License-Identifier: GPL-2.0-or-later OR LicenseRef-KDE-Accepted-LGPL
-+
-+import QtQuick
-+import QtQuick.Layouts
-+import QtQuick.Controls as QQC2
-+
-+import org.kde.kirigami as Kirigami
-+import org.kde.kcmutils as KCM
-+import org.kde.bigscreen as Bigscreen
-+import org.kde.bigscreen.webappskcm as WebAppsKCM
-+
-+Bigscreen.SidebarOverlay {
-+ id: root
-+ openFocusItem: nameButtonDelegate
-+
-+ property int modelIndex
-+ property string icon
-+ property string name
-+ property string url
-+ property string userAgent
-+
-+ header: ColumnLayout {
-+ spacing: Kirigami.Units.gridUnit
-+ Item { Layout.fillHeight: true }
-+
-+ Kirigami.Icon {
-+ Layout.alignment: Qt.AlignHCenter
-+ implicitWidth: 128
-+ implicitHeight: 128
-+ source: root.icon
-+ }
-+ QQC2.Label {
-+ text: root.name
-+
-+ Layout.fillWidth: true
-+ horizontalAlignment: Text.AlignHCenter
-+ wrapMode: Text.WordWrap
-+ maximumLineCount: 2
-+ elide: Text.ElideRight
-+ font.pixelSize: 32
-+ font.weight: Font.Light
-+ }
-+ }
-+
-+ content: ColumnLayout {
-+ spacing: Kirigami.Units.largeSpacing
-+ Keys.onLeftPressed: root.close()
-+
-+ Bigscreen.ButtonDelegate {
-+ id: nameButtonDelegate
-+ text: i18n("Name")
-+ description: root.name
-+
-+ KeyNavigation.down: urlButtonDelegate
-+ }
-+
-+ Bigscreen.ButtonDelegate {
-+ id: urlButtonDelegate
-+ text: i18n("URL")
-+ description: root.url
-+
-+ KeyNavigation.down: descriptionButtonDelegate
-+ }
-+
-+ Bigscreen.ButtonDelegate {
-+ id: descriptionButtonDelegate
-+ text: i18n("User Agent")
-+ description: root.userAgent.length > 0 ? root.userAgent : i18n("Default user agent")
-+
-+ KeyNavigation.down: deleteButtonDelegate
-+ }
-+ Item { Layout.fillHeight: true }
-+
-+ Bigscreen.ButtonDelegate {
-+ id: deleteButtonDelegate
-+ icon.name: 'delete'
-+ text: i18n("Delete")
-+
-+ onClicked: {
-+ deleteConfirmDialog.open();
-+ }
-+
-+ Bigscreen.Dialog {
-+ id: deleteConfirmDialog
-+ title: i18n("Delete web app %1?", root.name)
-+ standardButtons: Bigscreen.Dialog.Ok | Bigscreen.Dialog.Cancel
-+
-+ onAccepted: {
-+ deleteConfirmDialog.close();
-+ root.close();
-+ WebAppsKCM.WebAppManagerModel.removeApp(root.modelIndex);
-+ }
-+ onRejected: {
-+ deleteButtonDelegate.forceActiveFocus();
-+ }
-+ }
-+ }
-+ }
-+}
-\ No newline at end of file
-diff --git a/kcms/webapps/ui/main.qml b/kcms/webapps/ui/main.qml
-new file mode 100644
-index 0000000..5a2d289
---- /dev/null
-+++ b/kcms/webapps/ui/main.qml
-@@ -0,0 +1,138 @@
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org
-+// SPDX-License-Identifier: GPL-2.0-or-later OR LicenseRef-KDE-Accepted-LGPL
-+
-+import QtQuick
-+import QtQuick.Layouts
-+import QtQuick.Controls as QQC2
-+
-+import org.kde.kirigami as Kirigami
-+import org.kde.kcmutils as KCM
-+import org.kde.bigscreen as Bigscreen
-+import org.kde.bigscreen.webappskcm as WebAppsKCM
-+
-+Kirigami.ScrollablePage {
-+ id: root
-+
-+ title: i18n("Web Apps")
-+
-+ background: null
-+ leftPadding: Kirigami.Units.smallSpacing
-+ topPadding: Kirigami.Units.smallSpacing
-+ rightPadding: Kirigami.Units.smallSpacing
-+ bottomPadding: Kirigami.Units.smallSpacing
-+
-+ onActiveFocusChanged: {
-+ if (activeFocus) {
-+ addWebApp.forceActiveFocus();
-+ }
-+ }
-+
-+ ColumnLayout {
-+ KeyNavigation.left: root.KeyNavigation.left
-+ spacing: 0
-+
-+ Bigscreen.ButtonDelegate {
-+ id: addWebApp
-+
-+ onClicked: {
-+ addWebAppDialog.open()
-+ nameTextField.forceActiveFocus()
-+ }
-+
-+ text: i18n('Add web app')
-+ icon.name: 'list-add'
-+
-+ KeyNavigation.down: webAppListView
-+ }
-+
-+ QQC2.Label {
-+ text: i18n('Installed Web Apps')
-+ font.pixelSize: 22
-+ font.weight: Font.Normal
-+ Layout.fillWidth: true
-+ Layout.topMargin: Kirigami.Units.gridUnit
-+ Layout.bottomMargin: Kirigami.Units.gridUnit
-+ }
-+
-+ ListView {
-+ id: webAppListView
-+ Layout.fillWidth: true
-+
-+ implicitHeight: contentHeight
-+ model: WebAppsKCM.WebAppManagerModel
-+ currentIndex: 0
-+ spacing: Kirigami.Units.smallSpacing
-+
-+ delegate: Bigscreen.ButtonDelegate {
-+ id: delegate
-+
-+ icon.name: model.desktopIcon
-+ text: model.name
-+ description: model.url
-+ width: webAppListView.width
-+
-+ onClicked: {
-+ delegateInfoDialog.delegate = delegate
-+ delegateInfoDialog.modelIndex = model.index;
-+ delegateInfoDialog.icon = model.desktopIcon;
-+ delegateInfoDialog.name = model.name;
-+ delegateInfoDialog.url = model.url;
-+ delegateInfoDialog.userAgent = model.userAgent;
-+ delegateInfoDialog.open();
-+ }
-+ }
-+ }
-+
-+ Bigscreen.Dialog {
-+ id: addWebAppDialog
-+ title: i18n("Add web application")
-+ standardButtons: Bigscreen.Dialog.Ok | Bigscreen.Dialog.Cancel
-+ openFocusItem: nameTextField
-+
-+ onClosed: addWebApp.forceActiveFocus()
-+ onAccepted: {
-+ WebAppsKCM.WebAppCreator.addEntry(nameTextField.text, urlTextField.text, 'internet-web-browser', userAgentTextField.text);
-+ close();
-+ }
-+
-+ contentItem: ColumnLayout {
-+ spacing: Kirigami.Units.largeSpacing
-+
-+ Bigscreen.TextField {
-+ id: nameTextField
-+ Layout.fillWidth: true
-+ placeholderText: i18n("Name")
-+
-+ KeyNavigation.down: urlTextField
-+ }
-+ Bigscreen.TextField {
-+ id: urlTextField
-+ Layout.fillWidth: true
-+ placeholderText: i18n("URL")
-+
-+ KeyNavigation.down: userAgentTextField
-+ }
-+ Bigscreen.TextField {
-+ id: userAgentTextField
-+ Layout.fillWidth: true
-+ placeholderText: i18n("User Agent")
-+
-+ KeyNavigation.down: addWebAppDialog.footer
-+ }
-+ }
-+ }
-+
-+ WebAppInfoSidebar {
-+ id: delegateInfoDialog
-+
-+ property var delegate
-+ onClosed: {
-+ if (delegate) {
-+ delegate.forceActiveFocus();
-+ } else {
-+ root.forceActiveFocus();
-+ }
-+ }
-+ }
-+ }
-+}
-diff --git a/kcms/webapps/webappcreator.cpp b/kcms/webapps/webappcreator.cpp
-new file mode 100644
-index 0000000..e95ad10
---- /dev/null
-+++ b/kcms/webapps/webappcreator.cpp
-@@ -0,0 +1,102 @@
-+// SPDX-FileCopyrightText: 2020-2021 Jonah Brüchert <jbb.prv at gmx.de>
-+//
-+// SPDX-License-Identifier: LGPL-2.0-or-later
-+
-+#include "webappcreator.h"
-+#include "webappmanager.h"
-+
-+#include <QDebug>
-+#include <QDir>
-+#include <QFile>
-+#include <QProcess>
-+#include <QQmlEngine>
-+#include <QQuickImageProvider>
-+#include <QStandardPaths>
-+
-+#include <KConfigGroup>
-+#include <KDesktopFile>
-+
-+#include <QCoroSignal>
-+#include <QCoroTask>
-+
-+WebAppCreator::WebAppCreator(QObject *parent)
-+ : QObject(parent)
-+ , m_webAppMngr(WebAppManager::instance())
-+{
-+ connect(this, &WebAppCreator::websiteNameChanged, this, &WebAppCreator::existsChanged);
-+ connect(&m_webAppMngr, &WebAppManager::applicationsChanged, this, &WebAppCreator::existsChanged);
-+}
-+
-+bool WebAppCreator::exists() const
-+{
-+ return m_webAppMngr.exists(m_websiteName);
-+}
-+
-+const QString &WebAppCreator::websiteName() const
-+{
-+ return m_websiteName;
-+}
-+
-+void WebAppCreator::setWebsiteName(const QString &websiteName)
-+{
-+ m_websiteName = websiteName;
-+ Q_EMIT websiteNameChanged();
-+}
-+
-+QCoro::Task<> WebAppCreator::addEntry(const QString name, const QString url, const QString iconUrl, const QString &userAgent)
-+{
-+ // QPointer self = this;
-+ // auto image = co_await fetchIcon(iconUrl);
-+ // if (!self) {
-+ // co_return;
-+ // }
-+
-+ // m_webAppMngr.addApp(name, url, image, userAgent);
-+ m_webAppMngr.addApp(name, url, iconUrl, userAgent);
-+
-+ // Refresh homescreen entries
-+ QProcess buildsycoca;
-+ buildsycoca.setProgram(QStringLiteral("kbuildsycoca6"));
-+ buildsycoca.startDetached();
-+ co_return;
-+}
-+
-+QCoro::QmlTask WebAppCreator::createDesktopFile(const QString name, QString url, QString icon, const QString &userAgent)
-+{
-+ return addEntry(name, url, icon, userAgent);
-+}
-+
-+QCoro::Task<QImage> WebAppCreator::fetchIcon(const QString &url)
-+{
-+ auto *provider = static_cast<QQuickAsyncImageProvider *>(qmlEngine(this)->imageProvider(QStringLiteral("favicon")));
-+ if (!provider) {
-+ qDebug() << "Failed to access favicon provider";
-+ co_return QImage();
-+ }
-+
-+ const QStringView prefixFavicon = QStringView(u"image://favicon/");
-+ const QString providerIconName = url.mid(prefixFavicon.size());
-+
-+ const QSize szRequested;
-+
-+ switch (provider->imageType()) {
-+ case QQmlImageProviderBase::Image: {
-+ co_return provider->requestImage(providerIconName, nullptr, szRequested);
-+ }
-+ case QQmlImageProviderBase::Pixmap: {
-+ co_return provider->requestPixmap(providerIconName, nullptr, szRequested).toImage();
-+ }
-+ case QQmlImageProviderBase::Texture: {
-+ co_return provider->requestTexture(providerIconName, nullptr, szRequested)->image();
-+ }
-+ case QQmlImageProviderBase::ImageResponse: {
-+ auto response = provider->requestImageResponse(providerIconName, szRequested);
-+ co_await qCoro(response, &QQuickImageResponse::finished);
-+ co_return response->textureFactory()->image();
-+ }
-+ default:
-+ qDebug() << "Failed to save unhandled image type";
-+ }
-+
-+ co_return QImage();
-+}
-diff --git a/kcms/webapps/webappcreator.h b/kcms/webapps/webappcreator.h
-new file mode 100644
-index 0000000..3057d28
---- /dev/null
-+++ b/kcms/webapps/webappcreator.h
-@@ -0,0 +1,40 @@
-+// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QCoroQml>
-+#include <QCoroTask>
-+#include <QObject>
-+#include <qqmlregistration.h>
-+
-+class QQmlEngine;
-+class WebAppManager;
-+
-+class WebAppCreator : public QObject
-+{
-+ Q_OBJECT
-+ QML_ELEMENT
-+
-+ Q_PROPERTY(QString websiteName READ websiteName WRITE setWebsiteName NOTIFY websiteNameChanged)
-+ Q_PROPERTY(bool exists READ exists NOTIFY existsChanged)
-+
-+public:
-+ explicit WebAppCreator(QObject *parent = nullptr);
-+
-+ const QString &websiteName() const;
-+ void setWebsiteName(const QString &websiteName);
-+ Q_SIGNAL void websiteNameChanged();
-+
-+ bool exists() const;
-+ Q_SIGNAL void existsChanged();
-+
-+ Q_INVOKABLE QCoro::Task<> addEntry(const QString name, const QString url, const QString icon, const QString &userAgent);
-+ Q_INVOKABLE QCoro::QmlTask createDesktopFile(const QString name, QString url, QString icon, const QString &userAgent);
-+
-+private:
-+ QString m_websiteName;
-+ QCoro::Task<QImage> fetchIcon(const QString &url);
-+ WebAppManager &m_webAppMngr;
-+};
-diff --git a/kcms/webapps/webappmanager.cpp b/kcms/webapps/webappmanager.cpp
-new file mode 100644
-index 0000000..6377cd5
---- /dev/null
-+++ b/kcms/webapps/webappmanager.cpp
-@@ -0,0 +1,166 @@
-+// SPDX-FileCopyrightText: 2021 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "webappmanager.h"
-+
-+#include <QImage>
-+#include <QStandardPaths>
-+#include <QStringBuilder>
-+
-+#include <KConfigGroup>
-+#include <KDesktopFile>
-+#include <KSandbox>
-+
-+const QString USERAGENT_CFG_KEY = QStringLiteral("X-KDE-Bigscreen-UserAgent");
-+
-+WebAppManager::WebAppManager(QObject *parent)
-+ : QObject(parent)
-+ , m_desktopFileDirectory(desktopFileDirectory())
-+{
-+ const auto fileInfos = m_desktopFileDirectory.entryInfoList(QDir::Files);
-+
-+ // Likely almost all files in the directory are webapps, so this should be worth it
-+ m_webApps.reserve(fileInfos.size());
-+
-+ for (const auto &file : fileInfos) {
-+ // Make sure to only parse desktop files
-+ if (file.fileName().contains(QStringView(u".desktop"))) {
-+ KDesktopFile desktopFile(file.filePath());
-+
-+ auto configGroup = desktopFile.group(QStringLiteral("Desktop Entry"));
-+
-+ // Only handle desktop files referencing plasma-bigscreen
-+ if (configGroup.readEntry("Exec").contains(QStringView(u"plasma-bigscreen-webapp"))) {
-+ auto userAgent = configGroup.readEntry(USERAGENT_CFG_KEY);
-+ WebApp app{desktopFile.readName(), desktopFile.readIcon(), desktopFile.readUrl(), userAgent};
-+ m_webApps.push_back(std::move(app));
-+ }
-+ }
-+ }
-+}
-+
-+QString WebAppManager::desktopFileDirectory()
-+{
-+ auto dir = []() -> QString {
-+ if (KSandbox::isFlatpak()) {
-+ return qEnvironmentVariable("HOME") % u"/.local/share/applications/";
-+ }
-+ return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
-+ }();
-+
-+ QDir(dir).mkpath(QStringLiteral("."));
-+
-+ return dir;
-+}
-+
-+QString WebAppManager::iconDirectory()
-+{
-+ auto dir = []() -> QString {
-+ if (KSandbox::isFlatpak()) {
-+ return qEnvironmentVariable("HOME") % u"/.local/share/icons/hicolor/16x16/apps/";
-+ }
-+ return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/icons/hicolor/16x16/apps/");
-+ }();
-+ QDir(dir).mkpath(QStringLiteral("."));
-+
-+ return dir;
-+}
-+
-+const std::vector<WebApp> &WebAppManager::applications() const
-+{
-+ return m_webApps;
-+}
-+
-+void WebAppManager::addApp(const QString &name, const QString &url, const QImage &icon, const QString &userAgent)
-+{
-+ const QString filename = generateFileName(name);
-+
-+ icon.save(iconDirectory() % QDir::separator() % filename % u".png", "PNG");
-+ addApp(name, url, filename, userAgent);
-+}
-+
-+void WebAppManager::addApp(const QString &name, const QString &url, const QString &iconFileName, const QString &userAgent)
-+{
-+ const QString filename = generateFileName(name);
-+ const QString desktopFileName = generateDesktopFileName(name);
-+
-+ KConfig desktopFile(desktopFileDirectory() % QDir::separator() % desktopFileName, KConfig::SimpleConfig);
-+
-+ // TODO: maybe have program read options from .desktop file?
-+ // Currently, the user can inject and break the launch command
-+ QString exec = webAppCommand() + " --name \"" + name + "\"";
-+ if (!userAgent.isEmpty()) {
-+ exec += " --agent \"" + userAgent + "\"";
-+ }
-+ exec += " \"" + url + "\"";
-+
-+ auto desktopEntry = desktopFile.group(QStringLiteral("Desktop Entry"));
-+ desktopEntry.writeEntry(QStringLiteral("Type"), QStringLiteral("Application"));
-+ desktopEntry.writeEntry(QStringLiteral("URL"), url);
-+ desktopEntry.writeEntry(QStringLiteral("Name"), name);
-+ desktopEntry.writeEntry(QStringLiteral("Exec"), exec);
-+ desktopEntry.writeEntry(QStringLiteral("Icon"), iconFileName);
-+ desktopEntry.writeEntry(USERAGENT_CFG_KEY, userAgent);
-+
-+ m_webApps.push_back(WebApp{name, iconFileName, url, userAgent});
-+
-+ desktopFile.sync();
-+
-+ Q_EMIT applicationsChanged();
-+}
-+
-+bool WebAppManager::exists(const QString &name)
-+{
-+ const QString location = desktopFileDirectory();
-+ const QString filename = generateDesktopFileName(name);
-+
-+ return QFile::exists(location % QDir::separator() % filename);
-+}
-+
-+bool WebAppManager::removeApp(const QString &name)
-+{
-+ const QString location = desktopFileDirectory();
-+ const QString filename = generateDesktopFileName(name);
-+
-+ auto it = std::remove_if(m_webApps.begin(), m_webApps.end(), [&name](const WebApp &app) {
-+ return app.name == name;
-+ });
-+
-+ m_webApps.erase(it);
-+
-+ bool success = QFile::remove(location % QDir::separator() % filename);
-+ Q_EMIT applicationsChanged();
-+ return success;
-+}
-+
-+WebAppManager &WebAppManager::instance()
-+{
-+ static WebAppManager instance;
-+ return instance;
-+}
-+
-+QString WebAppManager::generateFileName(const QString &name)
-+{
-+ QString filename = name.toLower();
-+ filename.replace(QChar(u' '), QChar(u'_'));
-+ filename.remove(u'/');
-+ filename.remove(u'"');
-+ filename.remove(u'\'');
-+ filename.remove(u',');
-+ filename.remove(u'.');
-+ filename.remove(u'|');
-+ return filename;
-+}
-+
-+QString WebAppManager::generateDesktopFileName(const QString &name)
-+{
-+ return generateFileName(name) % u".desktop";
-+}
-+
-+QString WebAppManager::webAppCommand()
-+{
-+ return QStringLiteral("plasma-bigscreen-webapp");
-+}
-+
-+#include "moc_webappmanager.cpp"
-diff --git a/kcms/webapps/webappmanager.h b/kcms/webapps/webappmanager.h
-new file mode 100644
-index 0000000..764ebe5
---- /dev/null
-+++ b/kcms/webapps/webappmanager.h
-@@ -0,0 +1,48 @@
-+// SPDX-FileCopyrightText: 2021 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QDir>
-+#include <QObject>
-+
-+#include <memory>
-+
-+struct WebApp {
-+ QString name;
-+ QString icon;
-+ QString url;
-+ QString userAgent;
-+};
-+
-+class WebAppManager : public QObject
-+{
-+ Q_OBJECT
-+
-+public:
-+ explicit WebAppManager(QObject *parent = nullptr);
-+
-+ static QString desktopFileDirectory();
-+ static QString iconDirectory();
-+ const std::vector<WebApp> &applications() const;
-+
-+ void addApp(const QString &name, const QString &url, const QImage &icon, const QString &userAgent);
-+ void addApp(const QString &name, const QString &url, const QString &iconFileName, const QString &userAgent);
-+ bool exists(const QString &name);
-+ bool removeApp(const QString &name);
-+
-+ static WebAppManager &instance();
-+
-+Q_SIGNALS:
-+ void applicationsChanged();
-+
-+private:
-+ static QString generateFileName(const QString &name);
-+ static QString generateDesktopFileName(const QString &name);
-+ static QString webAppCommand();
-+
-+private:
-+ QDir m_desktopFileDirectory;
-+ std::vector<WebApp> m_webApps;
-+};
-diff --git a/kcms/webapps/webappmanagermodel.cpp b/kcms/webapps/webappmanagermodel.cpp
-new file mode 100644
-index 0000000..3799eba
---- /dev/null
-+++ b/kcms/webapps/webappmanagermodel.cpp
-@@ -0,0 +1,58 @@
-+// SPDX-FileCopyrightText: 2021 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "webappmanagermodel.h"
-+
-+#include "webappmanager.h"
-+
-+WebAppManagerModel::WebAppManagerModel(QObject *parent)
-+ : QAbstractListModel(parent)
-+ , m_webAppMngr(WebAppManager::instance())
-+{
-+}
-+
-+WebAppManagerModel::~WebAppManagerModel() = default;
-+
-+int WebAppManagerModel::rowCount(const QModelIndex &index) const
-+{
-+ return index.isValid() ? 0 : int(m_webAppMngr.applications().size());
-+}
-+
-+QVariant WebAppManagerModel::data(const QModelIndex &index, int role) const
-+{
-+ switch (role) {
-+ case Role::NameRole:
-+ return m_webAppMngr.applications()[index.row()].name;
-+ case Role::IconRole:
-+ return m_webAppMngr.applications()[index.row()].icon;
-+ // return QString(WebAppManager::iconDirectory() + QString(QDir::separator()) + m_webAppMngr.applications()[index.row()].icon);
-+ case Role::UrlRole:
-+ return m_webAppMngr.applications()[index.row()].url;
-+ case Role::UserAgentRole:
-+ return m_webAppMngr.applications()[index.row()].userAgent;
-+ }
-+
-+ Q_UNREACHABLE();
-+
-+ return {};
-+}
-+
-+QHash<int, QByteArray> WebAppManagerModel::roleNames() const
-+{
-+ return {
-+ {Role::NameRole, QByteArrayLiteral("name")},
-+ {Role::IconRole, QByteArrayLiteral("desktopIcon")},
-+ {Role::UrlRole, QByteArrayLiteral("url")},
-+ {Role::UserAgentRole, QByteArrayLiteral("userAgent")},
-+ };
-+}
-+
-+void WebAppManagerModel::removeApp(int index)
-+{
-+ beginRemoveRows({}, index, index);
-+ m_webAppMngr.removeApp(m_webAppMngr.applications()[index].name);
-+ endRemoveRows();
-+}
-+
-+#include "moc_webappmanagermodel.cpp"
-diff --git a/kcms/webapps/webappmanagermodel.h b/kcms/webapps/webappmanagermodel.h
-new file mode 100644
-index 0000000..5f350d4
---- /dev/null
-+++ b/kcms/webapps/webappmanagermodel.h
-@@ -0,0 +1,37 @@
-+// SPDX-FileCopyrightText: 2021 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QAbstractListModel>
-+
-+#include <qqmlregistration.h>
-+
-+class WebAppManager;
-+
-+class WebAppManagerModel : public QAbstractListModel
-+{
-+ Q_OBJECT
-+ QML_ELEMENT
-+
-+ enum Role {
-+ NameRole,
-+ IconRole,
-+ UrlRole,
-+ UserAgentRole
-+ };
-+
-+public:
-+ explicit WebAppManagerModel(QObject *parent = nullptr);
-+ ~WebAppManagerModel();
-+
-+ int rowCount(const QModelIndex &index) const override;
-+ QVariant data(const QModelIndex &index, int role) const override;
-+ QHash<int, QByteArray> roleNames() const override;
-+
-+ Q_INVOKABLE void removeApp(int index);
-+
-+private:
-+ WebAppManager &m_webAppMngr;
-+};
-diff --git a/kcms/webapps/webappskcm.cpp b/kcms/webapps/webappskcm.cpp
-new file mode 100644
-index 0000000..be7c55b
---- /dev/null
-+++ b/kcms/webapps/webappskcm.cpp
-@@ -0,0 +1,32 @@
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "webappskcm.h"
-+
-+#include "webappcreator.h"
-+#include "webappmanager.h"
-+#include "webappmanagermodel.h"
-+
-+WebAppsKCM::WebAppsKCM(QObject *parent, const KPluginMetaData &data)
-+ : KQuickConfigModule(parent, data)
-+{
-+ WebAppManager::instance();
-+ WebAppManagerModel *model = new WebAppManagerModel{this};
-+
-+ qmlRegisterSingletonType<WebAppManager>("org.kde.bigscreen.webappskcm", 1, 0, "WebAppManager", [this](QQmlEngine *, QJSEngine *) -> QObject * {
-+ return new WebAppManager{this};
-+ });
-+ qmlRegisterSingletonType<WebAppCreator>("org.kde.bigscreen.webappskcm", 1, 0, "WebAppCreator", [this](QQmlEngine *, QJSEngine *) -> QObject * {
-+ return new WebAppCreator{this};
-+ });
-+ qmlRegisterSingletonType<WebAppManagerModel>("org.kde.bigscreen.webappskcm", 1, 0, "WebAppManagerModel", [this](QQmlEngine *, QJSEngine *) -> QObject * {
-+ return new WebAppManagerModel{this};
-+ });
-+}
-+
-+WebAppsKCM::~WebAppsKCM() = default;
-+
-+K_PLUGIN_CLASS_WITH_JSON(WebAppsKCM, "kcm_mediacenter_webapps.json")
-+
-+#include "moc_webappskcm.cpp"
-+#include "webappskcm.moc"
-diff --git a/kcms/webapps/webappskcm.h b/kcms/webapps/webappskcm.h
-new file mode 100644
-index 0000000..45768eb
---- /dev/null
-+++ b/kcms/webapps/webappskcm.h
-@@ -0,0 +1,23 @@
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <KQuickConfigModule>
-+#include <QObject>
-+#include <QVariant>
-+
-+class WebAppsKCM : public KQuickConfigModule
-+{
-+ Q_OBJECT
-+
-+public:
-+ WebAppsKCM(QObject *parent, const KPluginMetaData &data);
-+ ~WebAppsKCM() override;
-+
-+public Q_SLOTS:
-+
-+Q_SIGNALS:
-+
-+private:
-+};
-diff --git a/webapp-viewer/CMakeLists.txt b/webapp-viewer/CMakeLists.txt
-new file mode 100644
-index 0000000..24c75d9
---- /dev/null
-+++ b/webapp-viewer/CMakeLists.txt
-@@ -0,0 +1,43 @@
-+# SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb at kaidan.im>
-+# SPDX-FileCopyrightText: 2020 Rinigus <rinigus.git at gmail.com>
-+# SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+# SPDX-License-Identifier: LGPL-2.0-or-later
-+
-+add_executable(plasma-bigscreen-webapp
-+ main.cpp
-+ useragent.cpp
-+ browsermanager.cpp
-+ webprofile.cpp
-+)
-+
-+ecm_add_qml_module(plasma-bigscreen-webapp-plugin URI org.kde.bigscreen.webapp.sources GENERATE_PLUGIN_SOURCE DEPENDENCIES QtQuick)
-+
-+# Include qml and js files within ./qml/
-+file(GLOB_RECURSE _qml_sources
-+ "qml/*.qml"
-+ "qml/*.js"
-+)
-+ecm_target_qml_sources(plasma-bigscreen-webapp-plugin SOURCES ${_qml_sources})
-+ecm_finalize_qml_module(plasma-bigscreen-webapp-plugin)
-+
-+ecm_add_qml_module(plasma-bigscreen-webapp
-+ GENERATE_PLUGIN_SOURCE
-+ URI org.kde.bigscreen.webapp
-+)
-+
-+target_compile_definitions(plasma-bigscreen-webapp PRIVATE -DQT_NO_CAST_FROM_ASCII)
-+target_link_libraries(plasma-bigscreen-webapp PRIVATE
-+ Qt::Core
-+ Qt::Qml
-+ Qt::Quick
-+ Qt::Widgets
-+ KF6::I18n
-+ KF6::CoreAddons
-+ KF6::Notifications
-+)
-+
-+target_link_libraries(plasma-bigscreen-webapp PRIVATE Qt::WebEngineCore Qt::WebEngineQuick)
-+
-+target_include_directories(plasma-bigscreen-webapp PRIVATE ${CMAKE_BINARY_DIR})
-+install(TARGETS plasma-bigscreen-webapp ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
-+
-diff --git a/webapp-viewer/browsermanager.cpp b/webapp-viewer/browsermanager.cpp
-new file mode 100644
-index 0000000..b866ef2
---- /dev/null
-+++ b/webapp-viewer/browsermanager.cpp
-@@ -0,0 +1,77 @@
-+// SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas at kde.org>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "browsermanager.h"
-+
-+#include <QDebug>
-+#include <QQmlEngine>
-+#include <QSettings>
-+#include <QStandardPaths>
-+#include <QUrl>
-+
-+BrowserManager *BrowserManager::s_instance = nullptr;
-+
-+BrowserManager::BrowserManager(QObject *parent)
-+ : QObject(parent)
-+{
-+}
-+
-+void BrowserManager::addToHistory(const QVariantMap &pagedata)
-+{
-+ // m_dbmanager->addToHistory(pagedata);
-+}
-+
-+void BrowserManager::removeFromHistory(const QString &url)
-+{
-+ // m_dbmanager->removeFromHistory(url);
-+}
-+
-+void BrowserManager::clearHistory()
-+{
-+ // m_dbmanager->clearHistory();
-+}
-+
-+void BrowserManager::updateLastVisited(const QString &url)
-+{
-+ // m_dbmanager->updateLastVisited(url);
-+}
-+
-+void BrowserManager::updateIcon(const QString &url, const QString &iconSource)
-+{
-+ auto *engine = qmlEngine(this);
-+ Q_ASSERT(engine);
-+ // TODO
-+ // m_dbmanager->updateIcon(engine, url, iconSource);
-+}
-+
-+QUrl BrowserManager::initialUrl() const
-+{
-+ return m_initialUrl;
-+}
-+
-+QString BrowserManager::tempDirectory() const
-+{
-+ return QStandardPaths::writableLocation(QStandardPaths::TempLocation);
-+}
-+
-+QString BrowserManager::downloadDirectory() const
-+{
-+ return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
-+}
-+
-+void BrowserManager::setInitialUrl(const QUrl &initialUrl)
-+{
-+ m_initialUrl = initialUrl;
-+ Q_EMIT initialUrlChanged();
-+}
-+
-+BrowserManager *BrowserManager::instance()
-+{
-+ if (!s_instance)
-+ s_instance = new BrowserManager();
-+
-+ return s_instance;
-+}
-+
-+#include "moc_browsermanager.cpp"
-diff --git a/webapp-viewer/browsermanager.h b/webapp-viewer/browsermanager.h
-new file mode 100644
-index 0000000..696275c
---- /dev/null
-+++ b/webapp-viewer/browsermanager.h
-@@ -0,0 +1,47 @@
-+
-+// SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas at kde.org>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QJSEngine>
-+#include <QObject>
-+#include <QQmlEngine>
-+#include <QUrl>
-+#include <QtQml/qqmlregistration.h>
-+
-+class BrowserManager : public QObject
-+{
-+ Q_OBJECT
-+ Q_PROPERTY(QUrl initialUrl READ initialUrl WRITE setInitialUrl NOTIFY initialUrlChanged)
-+ QML_ELEMENT
-+ QML_SINGLETON
-+
-+public:
-+ static BrowserManager *instance();
-+ static BrowserManager *create(QQmlEngine *, QJSEngine *)
-+ {
-+ return BrowserManager::instance();
-+ }
-+ QUrl initialUrl() const;
-+ void setInitialUrl(const QUrl &initialUrl);
-+
-+Q_SIGNALS:
-+ void updated();
-+ void initialUrlChanged();
-+
-+public Q_SLOTS:
-+ void addToHistory(const QVariantMap &pagedata);
-+ void removeFromHistory(const QString &url);
-+ void clearHistory();
-+ void updateLastVisited(const QString &url);
-+ void updateIcon(const QString &url, const QString &iconSource);
-+ QString tempDirectory() const;
-+ QString downloadDirectory() const;
-+
-+private:
-+ BrowserManager(QObject *parent = nullptr);
-+ QUrl m_initialUrl;
-+ static BrowserManager *s_instance;
-+};
-diff --git a/webapp-viewer/main.cpp b/webapp-viewer/main.cpp
-new file mode 100644
-index 0000000..ce1fda6
---- /dev/null
-+++ b/webapp-viewer/main.cpp
-@@ -0,0 +1,81 @@
-+// SPDX-FileCopyrightText: 2019 Jonah Brüchert <jbb.prv at gmx.de>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: LGPL-2.0-or-later
-+
-+#include <QApplication>
-+#include <QCommandLineOption>
-+#include <QCommandLineParser>
-+#include <QIcon>
-+#include <QQmlApplicationEngine>
-+#include <QUrl>
-+#include <QtQml>
-+#include <QtWebEngineQuick>
-+
-+#include <KAboutData>
-+#include <KLocalizedContext>
-+#include <KLocalizedQmlContext>
-+#include <KLocalizedString>
-+
-+#include "browsermanager.h"
-+#include "useragent.h"
-+
-+constexpr auto APPLICATION_ID = "org.kde.angelfish";
-+
-+int main(int argc, char *argv[])
-+{
-+ QGuiApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
-+
-+ KLocalizedString::setApplicationDomain("plasma-bigscreen");
-+
-+ // Setup QtWebEngine
-+ qputenv("QTWEBENGINE_DIALOG_SET", "QtQuickControls2");
-+ QtWebEngineQuick::initialize();
-+
-+ QApplication app(argc, argv);
-+
-+ // Command line parser
-+ QCommandLineOption agentOption{QStringLiteral("agent"),
-+ i18n("The user agent to browse with."),
-+ QStringLiteral("Mozilla/5.0 (Linux; Android 12) Cobalt/22.2.3-gold (PS4)")};
-+ QCommandLineOption nameOption{QStringLiteral("name"), i18n("The name of the web app"), QStringLiteral("webapp")};
-+
-+ QCommandLineParser parser;
-+ parser.addPositionalArgument(QStringLiteral("link"), i18n("link of website to launch"), QStringLiteral("[link]"));
-+ parser.addOption(agentOption);
-+ parser.addOption(nameOption);
-+ parser.addHelpOption();
-+ parser.process(app);
-+
-+ if (parser.positionalArguments().isEmpty()) {
-+ return 1;
-+ }
-+
-+ const QString link = parser.positionalArguments().constFirst();
-+ const QUrl initialUrl = QUrl::fromUserInput(link);
-+
-+ KAboutData aboutData(QStringLiteral("plasma-bigscreen-webapp"),
-+ parser.isSet(nameOption) ? parser.value(nameOption) : i18n("Webview"),
-+ QStringLiteral("0.1"),
-+ i18n("Plasma Bigscreen Webapp runtime"),
-+ KAboutLicense::GPL,
-+ i18n("Copyright 2025 Plasma developers"));
-+ QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral()));
-+
-+ KAboutData::setApplicationData(aboutData);
-+
-+ BrowserManager::instance()->setInitialUrl(initialUrl);
-+
-+ // QML loading
-+ QQmlApplicationEngine engine;
-+
-+ engine.setInitialProperties({{QStringLiteral("userAgent"), parser.isSet(agentOption) ? parser.value(agentOption) : QString{}}});
-+ engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
-+ engine.loadFromModule(QStringLiteral("org.kde.bigscreen.webapp.sources"), QStringLiteral("WebApp"));
-+
-+ // Error handling
-+ if (engine.rootObjects().isEmpty()) {
-+ return -1;
-+ }
-+
-+ return app.exec();
-+}
-diff --git a/webapp-viewer/qml/WebApp.qml b/webapp-viewer/qml/WebApp.qml
-new file mode 100644
-index 0000000..7b4a42e
---- /dev/null
-+++ b/webapp-viewer/qml/WebApp.qml
-@@ -0,0 +1,79 @@
-+// SPDX-FileCopyrightText: 2014-2015 Sebastian Kügler <sebas at kde.org>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+import QtQuick
-+import QtQuick.Window
-+import QtQuick.Layouts
-+import QtWebEngine
-+
-+import org.kde.kirigami as Kirigami
-+import org.kde.bigscreen.webapp
-+
-+Kirigami.ApplicationWindow {
-+ id: webBrowser
-+ title: currentWebView.title
-+
-+ required property string userAgent
-+
-+ minimumWidth: Kirigami.Units.gridUnit * 15
-+ minimumHeight: Kirigami.Units.gridUnit * 15
-+
-+ pageStack.globalToolBar.showNavigationButtons: Kirigami.ApplicationHeaderStyle.NoNavigationButtons
-+ pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None
-+
-+ // Main Page
-+ pageStack.initialPage: Kirigami.Page {
-+ id: rootPage
-+ leftPadding: 0
-+ rightPadding: 0
-+ topPadding: 0
-+ bottomPadding: 0
-+ globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
-+
-+ Kirigami.ColumnView.fillWidth: true
-+ Kirigami.ColumnView.pinned: true
-+ Kirigami.ColumnView.preventStealing: true
-+
-+ WebAppView {
-+ // ID for compatibility with components
-+ id: currentWebView
-+ anchors.fill: parent
-+ url: BrowserManager.initialUrl
-+ userAgent: webBrowser.userAgent
-+ }
-+
-+ // Container for the progress bar
-+ Item {
-+ id: progressItem
-+
-+ height: Math.round(Kirigami.Units.gridUnit / 6)
-+ z: 99
-+ anchors {
-+ bottom: parent.bottom
-+ bottomMargin: -Math.round(height / 2)
-+ left: webBrowser.left
-+ right: webBrowser.right
-+ }
-+
-+ opacity: currentWebView.loading ? 1 : 0
-+ Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad; } }
-+
-+ Rectangle {
-+ color: Kirigami.Theme.highlightColor
-+
-+ width: Math.round((currentWebView.loadProgress / 100) * parent.width)
-+ anchors {
-+ top: parent.top
-+ left: parent.left
-+ bottom: parent.bottom
-+ }
-+ }
-+ }
-+
-+ Loader {
-+ id: sheetLoader
-+ }
-+ }
-+}
-+
-diff --git a/webapp-viewer/qml/WebAppView.qml b/webapp-viewer/qml/WebAppView.qml
-new file mode 100644
-index 0000000..6f851d4
---- /dev/null
-+++ b/webapp-viewer/qml/WebAppView.qml
-@@ -0,0 +1,31 @@
-+// SPDX-FileCopyrightText: 2014-2015 Sebastian Kügler <sebas at kde.org>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+import QtQuick
-+import QtQuick.Controls as QQC2
-+import QtQuick.Layouts
-+
-+import org.kde.kirigami as Kirigami
-+import org.kde.bigscreen.webapp as WebApp
-+
-+WebView {
-+ id: webEngineView
-+ property string userAgent
-+
-+ onUserAgentChanged: WebApp.UserAgent.userAgent = userAgent
-+
-+ profile: WebApp.WebProfile {
-+ httpUserAgent: WebApp.UserAgent.userAgent
-+ offTheRecord: false
-+ storageName: "plasma-bigscreen-webapp"
-+
-+ onHttpUserAgentChanged: console.log("User agent set: " + httpUserAgent)
-+ }
-+
-+ isAppView: true
-+
-+ onNewWindowRequested: {
-+ Qt.openUrlExternally(request.requestedUrl);
-+ }
-+}
-diff --git a/webapp-viewer/qml/WebView.qml b/webapp-viewer/qml/WebView.qml
-new file mode 100644
-index 0000000..2c5ab8d
---- /dev/null
-+++ b/webapp-viewer/qml/WebView.qml
-@@ -0,0 +1,461 @@
-+// SPDX-FileCopyrightText: 2014-2015 Sebastian Kügler <sebas at kde.org>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+import QtQuick
-+import QtQuick.Controls as QQC2
-+import QtQuick.Layouts
-+import QtWebEngine
-+
-+import org.kde.kirigami as Kirigami
-+
-+import org.kde.bigscreen.webapp as WebApp
-+
-+WebEngineView {
-+ id: webEngineView
-+
-+ // int, but we want nullability
-+ property var errorCode: null
-+ property var errorDomain: null
-+ property string errorString: ""
-+
-+ property bool privateMode: false
-+
-+ property var userAgent: WebApp.UserAgent
-+
-+ // loadingActive property is set to true when loading is started
-+ // and turned to false only after succesful or failed loading. It
-+ // is possible to set it to false by calling stopLoading method.
-+ //
-+ // The property was introduced as it triggers visibility of the webEngineView
-+ // in the other parts of the code. When using loading that is linked
-+ // to visibility, stop/start loading was observed in some conditions. It looked as if
-+ // there is an internal optimization of webengine in the case of parallel
-+ // loading of several pages that could use visibility as one of the decision
-+ // making parameters.
-+ property bool loadingActive: false
-+
-+ // reloadOnVisible property ensures that the view has been always
-+ // loaded at least once while it is visible. When the view is loaded
-+ // while visible is set to false, there, what appears to be Chromium
-+ // optimizations that can disturb the loading.
-+ property bool reloadOnVisible: true
-+
-+ // Profiles of WebViews are shared among all views of the same type (regular or
-+ // private). However, within each group of tabs, we can have some tabs that are
-+ // using mobile or desktop user agent. To avoid loading a page with the wrong
-+ // user agent, the agent is checked in the beginning of the loading at onLoadingChanged
-+ // handler. If the user agent is wrong, loading is stopped and reloadOnMatchingAgents
-+ // property is set to true. As soon as the agent is correct, the page is loaded.
-+ property bool reloadOnMatchingAgents: false
-+
-+ // Used to follow whether agents match
-+ property bool agentsMatch: profile.httpUserAgent === userAgent.userAgent
-+
-+ // URL that was requested and should be used
-+ // as a base for user interaction. It reflects
-+ // last request (successful or failed)
-+ property url requestedUrl: url
-+
-+ property int findInPageResultIndex
-+ property int findInPageResultCount
-+
-+ // Used to hide certain context menu items
-+ property bool isAppView: false
-+
-+ // url to keep last url to return from reader mode
-+ property url readerSourceUrl
-+
-+ // string to keep last title to return from reader mode
-+ property string readerTitle
-+
-+ // Used for pdf generated to preview before print
-+ property url printPreviewUrl: ""
-+ property bool generatingPdf: false
-+ property int printedPageOrientation: WebEngineView.Portrait
-+ property int printedPageSizeId: WebEngineView.A4
-+
-+ Shortcut {
-+ enabled: webEngineView.isFullScreen
-+ sequence: "Esc"
-+ onActivated: webEngineView.fullScreenCancelled();
-+ }
-+
-+ settings {
-+ autoLoadImages: true
-+ javascriptEnabled: true
-+ // Disable builtin error pages in favor of our own
-+ errorPageEnabled: false
-+ // Load larger touch icons
-+ touchIconsEnabled: true
-+ // Disable scrollbars on mobile
-+ showScrollBars: true
-+ // Generally allow screen sharing, still needs permission from the user
-+ screenCaptureEnabled: true
-+ // Enables a web page to request that one of its HTML elements be made to occupy the user's entire screen
-+ fullScreenSupportEnabled: true
-+ // Turns on printing of CSS backgrounds when printing a web page
-+ printElementBackgrounds: false
-+ }
-+
-+ focus: true
-+ onLoadingChanged: loadRequest => {
-+ print(" url: " + loadRequest.url + " " + loadRequest.status)
-+
-+ /* Handle
-+ * - WebEngineView::LoadStartedStatus,
-+ * - WebEngineView::LoadStoppedStatus,
-+ * - WebEngineView::LoadSucceededStatus and
-+ * - WebEngineView::LoadFailedStatus
-+ */
-+ var ec = null;
-+ var es = "";
-+ var ed = null;
-+ if (loadRequest.status === WebEngineView.LoadStartedStatus) {
-+ if (profile.httpUserAgent !== userAgent.userAgent) {
-+ //print("Mismatch of user agents, will load later " + loadRequest.url);
-+ reloadOnMatchingAgents = true;
-+ stopLoading();
-+ } else {
-+ loadingActive = true;
-+ }
-+ }
-+ if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
-+ if (!privateMode) {
-+ const request = {
-+ url: currentWebView.url,
-+ title: currentWebView.title,
-+ icon: currentWebView.icon
-+ }
-+
-+ WebApp.BrowserManager.addToHistory(request);
-+ WebApp.BrowserManager.updateLastVisited(currentWebView.url);
-+ }
-+
-+ ec = null;
-+ es = "";
-+ ed = null;
-+ loadingActive = false;
-+ }
-+ if (loadRequest.status === WebEngineView.LoadFailedStatus) {
-+ print("Load failed: " + loadRequest.errorCode + " " + loadRequest.errorString);
-+ print("Load failed url: " + loadRequest.url + " " + url);
-+ ec = loadRequest.errorCode;
-+ es = loadRequest.errorString;
-+ ed = loadRequest.errorDomain
-+ loadingActive = false;
-+
-+ // update requested URL only after its clear that it fails.
-+ // Otherwise, its updated as a part of url property update.
-+ if (requestedUrl !== loadRequest.url)
-+ requestedUrl = loadRequest.url;
-+ }
-+ errorCode = ec;
-+ errorDomain = ed;
-+ errorString = es;
-+ }
-+
-+ Component.onCompleted: {
-+ print("WebView completed.");
-+ print("Settings: " + webEngineView.settings);
-+ }
-+
-+ onIconChanged: {
-+ if (icon && !privateMode) {
-+ WebApp.BrowserManager.updateIcon(url, icon)
-+ }
-+ }
-+ onNewWindowRequested: request => {
-+ // Just open in a browser for now
-+ Qt.openUrlExternally(request.requestedUrl.toString());
-+
-+ // // If a new window is requested, just open it
-+ // if (request.userInitiated) {
-+ // tabsModel.newTab(request.requestedUrl.toString())
-+ // showPassiveNotification(i18nc("@info:status", "Website was opened in a new tab"))
-+ // } else {
-+ // // TODO: should we allow user input?
-+ // questionLoader.setSource("NewTabQuestion.qml")
-+ // questionLoader.item.url = request.requestedUrl
-+ // questionLoader.item.visible = true
-+ // }
-+ }
-+ onUrlChanged: {
-+ if (requestedUrl !== url) {
-+ requestedUrl = url;
-+ }
-+ }
-+
-+ onFullScreenRequested: request => {
-+ if (request.toggleOn) {
-+ webBrowser.showFullScreen()
-+ const message = i18nc("@info:status", "Entered Full Screen mode")
-+ const actionText = i18nc("@action:button", "Exit Full Screen (Esc)")
-+ showPassiveNotification(message, "short", actionText, function() { webEngineView.fullScreenCancelled() });
-+ } else {
-+ webBrowser.showNormal()
-+ }
-+
-+ request.accept()
-+ }
-+
-+ onContextMenuRequested: request => {
-+ request.accepted = true // Make sure QtWebEngine doesn't show its own context menu.
-+ contextMenu.request = request
-+ contextMenu.x = request.position.x
-+ contextMenu.y = request.position.y
-+ contextMenu.open()
-+ }
-+
-+ onAuthenticationDialogRequested: request => {
-+ request.accepted = true
-+ sheetLoader.setSource("AuthSheet.qml")
-+ sheetLoader.item.request = request
-+ sheetLoader.item.open()
-+ }
-+
-+ onFeaturePermissionRequested: (securityOrigin, feature) => {
-+ let newQuestion = rootPage.questions.newPermissionQuestion()
-+ newQuestion.permission = feature
-+ newQuestion.origin = securityOrigin
-+ newQuestion.visible = true
-+ }
-+
-+ onJavaScriptDialogRequested: request => {
-+ request.accepted = true;
-+ sheetLoader.setSource("JavaScriptDialogSheet.qml");
-+ sheetLoader.item.request = request;
-+ sheetLoader.item.open();
-+ }
-+
-+ onFindTextFinished: result => {
-+ findInPageResultIndex = result.activeMatch;
-+ findInPageResultCount = result.numberOfMatches;
-+ }
-+
-+ onVisibleChanged: {
-+ if (visible && reloadOnVisible) {
-+ // see description of reloadOnVisible above for reasoning
-+ reloadOnVisible = false;
-+ reload();
-+ }
-+ }
-+
-+ onAgentsMatchChanged: {
-+ if (agentsMatch && reloadOnMatchingAgents) {
-+ // see description of reloadOnMatchingAgents above for reasoning
-+ reloadOnMatchingAgents = false;
-+ reload();
-+ }
-+ }
-+
-+ onCertificateError: error => {
-+ error.defer();
-+ errorHandler.enqueue(error);
-+ }
-+
-+ function findInPageForward(text) {
-+ findText(text);
-+ }
-+
-+ function stopLoading() {
-+ loadingActive = false;
-+ stop();
-+ }
-+
-+ onLinkHovered: hoveredUrl => hoveredLink.text = hoveredUrl
-+
-+ QQC2.Label {
-+ id: hoveredLink
-+ visible: text.length > 0
-+ z: 2
-+ anchors.bottom: parent.bottom
-+ anchors.left: parent.left
-+ leftPadding: Kirigami.Units.smallSpacing
-+ rightPadding: Kirigami.Units.smallSpacing
-+ color: Kirigami.Theme.textColor
-+ font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
-+
-+ background: Rectangle {
-+ anchors.fill: parent
-+ color: Kirigami.Theme.backgroundColor
-+ }
-+ }
-+
-+ QQC2.Menu {
-+ id: contextMenu
-+ property ContextMenuRequest request
-+ property bool isValidUrl: contextMenu.request && contextMenu.request.linkUrl != "" // not strict equality
-+ property bool isAudio: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeAudio
-+ property bool isImage: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeImage
-+ property bool isVideo: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeVideo
-+ property real playbackRate: 100
-+
-+ onAboutToShow: {
-+ if (webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)) {
-+ const point = contextMenu.request.x + ', ' + contextMenu.request.y
-+ const js = 'document.elementFromPoint(' + point + ').playbackRate * 100;'
-+ webEngineView.runJavaScript(js, function(result) { contextMenu.playbackRate = result })
-+ }
-+ }
-+
-+ QQC2.MenuItem {
-+ visible: contextMenu.isAudio || contextMenu.isVideo
-+ height: visible ? implicitHeight : 0
-+ text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaPaused
-+ ? i18nc("@action:inmenu", "Play")
-+ : i18nc("@action:inmenu", "Pause")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaPlayPause)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaHasAudio
-+ height: visible ? implicitHeight : 0
-+ text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaMuted
-+ ? i18nc("@action:inmenu", "Unmute")
-+ : i18nc("@action:inmenu", "Mute")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaMute)
-+ }
-+ QQC2.MenuItem {
-+ visible: webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)
-+ height: visible ? implicitHeight : 0
-+ contentItem: RowLayout {
-+ QQC2.Label {
-+ Layout.leftMargin: Kirigami.Units.largeSpacing
-+ Layout.fillWidth: true
-+ text: i18nc("@label", "Speed")
-+ }
-+ QQC2.SpinBox {
-+ Layout.rightMargin: Kirigami.Units.largeSpacing
-+ value: contextMenu.playbackRate
-+ from: 25
-+ to: 1000
-+ stepSize: 25
-+ onValueModified: {
-+ contextMenu.playbackRate = value
-+ const point = contextMenu.request.x + ', ' + contextMenu.request.y
-+ const js = 'document.elementFromPoint(' + point + ').playbackRate = ' + contextMenu.playbackRate / 100 + ';'
-+ webEngineView.runJavaScript(js)
-+ }
-+ textFromValue: function(value, locale) {
-+ return Number(value / 100).toLocaleString(locale, 'f', 2)
-+ }
-+ }
-+ }
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isAudio || contextMenu.isVideo
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Loop")
-+ checked: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaLoop
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaLoop)
-+ }
-+ QQC2.MenuItem {
-+ visible: webEngineView.settings.javascriptEnabled && contextMenu.isVideo
-+ height: visible ? implicitHeight : 0
-+ text: webEngineView.isFullScreen ? i18nc("@action:inmenu", "Exit Fullscreen") : i18nc("@action:inmenu", "Enter Fullscreen")
-+ onTriggered: {
-+ const point = contextMenu.request.x + ', ' + contextMenu.request.y
-+ const js = webEngineView.isFullScreen
-+ ? 'document.exitFullscreen()'
-+ : 'document.elementFromPoint(' + point + ').requestFullscreen()'
-+ webEngineView.runJavaScript(js)
-+ }
-+ }
-+ QQC2.MenuItem {
-+ visible: webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)
-+ height: visible ? implicitHeight : 0
-+ text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaControls
-+ ? i18nc("@action:inmenu", "Hide Controls")
-+ : i18nc("@action:inmenu", "Show Controls")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaControls)
-+ }
-+ QQC2.MenuSeparator { visible: contextMenu.isAudio || contextMenu.isVideo }
-+ QQC2.MenuItem {
-+ visible: (contextMenu.isAudio || contextMenu.isVideo) && contextMenu.request.mediaUrl !== currentWebView.url
-+ height: visible ? implicitHeight : 0
-+ text: webEngineView.isAppView
-+ ? contextMenu.isVideo ? i18nc("@action:inmenu", "Open Video") : i18nc("@action:inmenu", "Open Audio")
-+ : contextMenu.isVideo ? i18nc("@action:inmenu", "Open Video in New Tab") : i18nc("@action:inmenu", "Open Audio in New Tab")
-+ onTriggered: {
-+ Qt.openUrlExternally(contextMenu.request.mediaUrl);
-+ }
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isVideo
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Save Video")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadMediaToDisk)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isVideo
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Copy Video Link")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyMediaUrlToClipboard)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isImage && contextMenu.request.mediaUrl !== currentWebView.url
-+ height: visible ? implicitHeight : 0
-+ text: webEngineView.isAppView ? i18nc("@action:inmenu", "Open Image") : i18nc("@action:inmenu", "Open Image in New Tab")
-+ onTriggered: {
-+ Qt.openUrlExternally(contextMenu.request.mediaUrl);
-+ }
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isImage
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Save Image")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadImageToDisk)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isImage
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Copy Image")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyImageToClipboard)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.isImage
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Copy Image Link")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyImageUrlToClipboard)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && contextMenu.isValidUrl
-+ height: visible ? implicitHeight : 0
-+ text: webEngineView.isAppView ? i18nc("@action:inmenu", "Open Link") : i18nc("@action:inmenu", "Open Link in New Window")
-+ onTriggered: {
-+ Qt.openUrlExternally(contextMenu.request.linkUrl);
-+ }
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && contextMenu.isValidUrl
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Save Link")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadLinkToDisk)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && contextMenu.isValidUrl
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Copy Link")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyLinkToClipboard)
-+ }
-+ QQC2.MenuSeparator { visible: contextMenu.request && contextMenu.isValidUrl }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanCopy) && contextMenu.request.mediaUrl == ""
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Copy")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.Copy)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanCut)
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Cut")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.Cut)
-+ }
-+ QQC2.MenuItem {
-+ visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanPaste)
-+ height: visible ? implicitHeight : 0
-+ text: i18nc("@action:inmenu", "Paste")
-+ onTriggered: webEngineView.triggerWebAction(WebEngineView.Paste)
-+ }
-+ }
-+}
-diff --git a/webapp-viewer/useragent.cpp b/webapp-viewer/useragent.cpp
-new file mode 100644
-index 0000000..3908499
---- /dev/null
-+++ b/webapp-viewer/useragent.cpp
-@@ -0,0 +1,59 @@
-+// SPDX-FileCopyrightText: 2021 Jonah Brüchert <jbb at kaidan.im>
-+// SPDX-FileCopyrightText: 2020 Rinigus <rinigus.git at gmail.com>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "useragent.h"
-+
-+#include <QQuickWebEngineProfile>
-+
-+UserAgent *UserAgent::s_instance = nullptr;
-+
-+UserAgent::UserAgent(QObject *parent)
-+ : QObject(parent)
-+ , m_defaultProfile(QQuickWebEngineProfile::defaultProfile())
-+ , m_defaultUserAgent(m_defaultProfile->httpUserAgent())
-+ , m_chromeVersion(extractValueFromAgent(u"Chrome"))
-+ , m_appleWebKitVersion(extractValueFromAgent(u"AppleWebKit"))
-+ , m_webEngineVersion(extractValueFromAgent(u"QtWebEngine"))
-+ , m_safariVersion(extractValueFromAgent(u"Safari"))
-+{
-+}
-+
-+QString UserAgent::userAgent() const
-+{
-+ if (m_userAgent.isEmpty()) {
-+ return QStringView(
-+ u"Mozilla/5.0 (%1) AppleWebKit/%2 (KHTML, like Gecko) QtWebEngine/%3 "
-+ u"Chrome/%4 %5 Safari/%6")
-+ .arg(u"X11; Linux x86_64", m_appleWebKitVersion, m_webEngineVersion, m_chromeVersion, u"Desktop", m_safariVersion);
-+ }
-+
-+ return m_userAgent;
-+}
-+
-+void UserAgent::setUserAgent(QString userAgent)
-+{
-+ m_userAgent = userAgent;
-+ Q_EMIT userAgentChanged();
-+}
-+
-+QStringView UserAgent::extractValueFromAgent(const QStringView key)
-+{
-+ const int index = m_defaultUserAgent.indexOf(key) + key.length() + 1;
-+ int endIndex = m_defaultUserAgent.indexOf(u' ', index);
-+ if (endIndex == -1) {
-+ endIndex = m_defaultUserAgent.size();
-+ }
-+ return QStringView(m_defaultUserAgent).mid(index, endIndex - index);
-+}
-+
-+UserAgent *UserAgent::instance()
-+{
-+ if (!s_instance)
-+ s_instance = new UserAgent();
-+
-+ return s_instance;
-+}
-+
-+#include "moc_useragent.cpp"
-diff --git a/webapp-viewer/useragent.h b/webapp-viewer/useragent.h
-new file mode 100644
-index 0000000..6fe88ea
---- /dev/null
-+++ b/webapp-viewer/useragent.h
-@@ -0,0 +1,47 @@
-+// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb at kaidan.im>
-+// SPDX-FileCopyrightText: 2020 Rinigus <rinigus.git at gmail.com>
-+// SPDX-FileCopyrightText: 2025 Devin Lin <devin at kde.org>
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QJSEngine>
-+#include <QObject>
-+#include <QQmlEngine>
-+#include <QtQml/qqmlregistration.h>
-+
-+class QQuickWebEngineProfile;
-+
-+class UserAgent : public QObject
-+{
-+ Q_OBJECT
-+ Q_PROPERTY(QString userAgent READ userAgent WRITE setUserAgent NOTIFY userAgentChanged)
-+ QML_ELEMENT
-+ QML_SINGLETON
-+
-+public:
-+ static UserAgent *instance();
-+ static UserAgent *create(QQmlEngine *, QJSEngine *)
-+ {
-+ return UserAgent::instance();
-+ }
-+ explicit UserAgent(QObject *parent = nullptr);
-+ QString userAgent() const;
-+ void setUserAgent(QString userAgent);
-+
-+Q_SIGNALS:
-+ void userAgentChanged();
-+
-+private:
-+ QStringView extractValueFromAgent(const QStringView key);
-+
-+ const QQuickWebEngineProfile *m_defaultProfile;
-+ const QString m_defaultUserAgent;
-+ const QStringView m_chromeVersion;
-+ const QStringView m_appleWebKitVersion;
-+ const QStringView m_webEngineVersion;
-+ const QStringView m_safariVersion;
-+
-+ QString m_userAgent;
-+ static UserAgent *s_instance;
-+};
-diff --git a/webapp-viewer/webprofile.cpp b/webapp-viewer/webprofile.cpp
-new file mode 100644
-index 0000000..d44a658
---- /dev/null
-+++ b/webapp-viewer/webprofile.cpp
-@@ -0,0 +1,66 @@
-+// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#include "webprofile.h"
-+
-+#include <KLocalizedString>
-+#include <QGuiApplication>
-+#include <QQuickItem>
-+#include <QQuickWindow>
-+#include <QWebEngineNotification>
-+
-+#include <KNotification>
-+
-+class QQuickWebEngineDownloadRequest : public DownloadItem
-+{
-+};
-+
-+WebProfile::WebProfile(QObject *parent)
-+ : QQuickWebEngineProfile(parent)
-+ , m_urlInterceptor(nullptr)
-+{
-+ connect(this, &QQuickWebEngineProfile::downloadRequested, this, &WebProfile::handleDownload);
-+ connect(this, &QQuickWebEngineProfile::downloadFinished, this, &WebProfile::handleDownloadFinished);
-+ connect(this, &QQuickWebEngineProfile::presentNotification, this, &WebProfile::showNotification);
-+}
-+
-+void WebProfile::handleDownload(QQuickWebEngineDownloadRequest *downloadItem)
-+{
-+ // TODO: do we handle downloads?
-+}
-+
-+void WebProfile::handleDownloadFinished(DownloadItem *downloadItem)
-+{
-+ // TODO: do we handle downloads?
-+}
-+
-+void WebProfile::showNotification(QWebEngineNotification *webNotification)
-+{
-+ auto *notification = new KNotification(QStringLiteral("web-notification"));
-+ notification->setComponentName(QStringLiteral("plasma-bigscreen"));
-+ notification->setTitle(webNotification->title());
-+ notification->setText(webNotification->message());
-+ notification->setPixmap(QPixmap::fromImage(webNotification->icon()));
-+
-+ connect(notification, &KNotification::closed, webNotification, &QWebEngineNotification::close);
-+
-+ auto defaultAction = notification->addDefaultAction(i18n("Open"));
-+ connect(defaultAction, &KNotificationAction::activated, webNotification, &QWebEngineNotification::click);
-+
-+ notification->sendEvent();
-+}
-+
-+QWebEngineUrlRequestInterceptor *WebProfile::urlInterceptor() const
-+{
-+ return m_urlInterceptor;
-+}
-+
-+void WebProfile::setUrlInterceptor(QWebEngineUrlRequestInterceptor *urlRequestInterceptor)
-+{
-+ setUrlRequestInterceptor(urlRequestInterceptor);
-+ m_urlInterceptor = urlRequestInterceptor;
-+ Q_EMIT urlInterceptorChanged();
-+}
-+
-+#include "moc_webprofile.cpp"
-diff --git a/webapp-viewer/webprofile.h b/webapp-viewer/webprofile.h
-new file mode 100644
-index 0000000..066d277
---- /dev/null
-+++ b/webapp-viewer/webprofile.h
-@@ -0,0 +1,44 @@
-+// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb at kaidan.im>
-+//
-+// SPDX-License-Identifier: GPL-2.0-or-later
-+
-+#pragma once
-+
-+#include <QObject>
-+#include <QQuickItem>
-+#include <QQuickWebEngineProfile>
-+#include <QWebEngineDownloadRequest>
-+#include <QWebEngineUrlRequestInterceptor>
-+#include <QtQml/qqmlregistration.h>
-+
-+using DownloadItem = QWebEngineDownloadRequest;
-+
-+class QWebEngineNotification;
-+class QQuickItem;
-+class QWebEngineUrlRequestInterceptor;
-+
-+class WebProfile : public QQuickWebEngineProfile
-+{
-+ Q_OBJECT
-+
-+ Q_PROPERTY(QWebEngineUrlRequestInterceptor *urlInterceptor WRITE setUrlInterceptor READ urlInterceptor NOTIFY urlInterceptorChanged)
-+
-+ QML_ELEMENT
-+
-+public:
-+ explicit WebProfile(QObject *parent = nullptr);
-+
-+ Q_SIGNAL void urlInterceptorChanged();
-+
-+ QWebEngineUrlRequestInterceptor *urlInterceptor() const;
-+ void setUrlInterceptor(QWebEngineUrlRequestInterceptor *urlRequestInterceptor);
-+
-+private:
-+ void handleDownload(QQuickWebEngineDownloadRequest *downloadItem);
-+ void handleDownloadFinished(DownloadItem *downloadItem);
-+ void showNotification(QWebEngineNotification *webNotification);
-+
-+ // A valid property needs a read function, and there is no getter in QQuickWebEngineProfile
-+ // so store a pointer ourselves
-+ QWebEngineUrlRequestInterceptor *m_urlInterceptor;
-+};
More information about the Neon-commits
mailing list