[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