diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4df1bb..105f4d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,14 @@ set(angelfish_SRCS main.cpp browsermanager.cpp urlmodel.cpp urlfilterproxymodel.cpp + useragent.cpp ) qt5_add_resources(RESOURCES resources.qrc) add_executable(angelfish ${angelfish_SRCS} ${RESOURCES}) target_link_libraries(angelfish Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg Qt5::WebEngine KF5::I18n) install(TARGETS angelfish ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/contents/ui/WebView.qml b/src/contents/ui/WebView.qml index 7d93c4f..4152d00 100644 --- a/src/contents/ui/WebView.qml +++ b/src/contents/ui/WebView.qml @@ -1,224 +1,204 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.4 as Controls import QtQuick.Window 2.1 import QtQuick.Layouts 1.3 - import QtWebEngine 1.7 import org.kde.kirigami 2.4 as Kirigami +import org.kde.mobile.angelfish 1.0 + WebEngineView { id: webEngineView property string errorCode: "" property string errorString: "" - /** - * User agent used on mobile devices - */ - readonly property string mobileUserAgent: "Mozilla/5.0 (Linux; Plasma Mobile, like Android 9.0 ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3770.116 Mobile Safari/537.36" - - /** - * User agent used on desktop devices, - * Defaults to QtWebEngine's default (it is only supported on desktop devices by Qt currently) - */ - readonly property string desktopUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.14.0 Chrome/75.0.3770.116 Safari/537.36" + property alias userAgent: userAgent width: pageWidth height: pageHeight - state: Kirigami.Settings.isMobile ? "mobile" : "desktop" - - states: [ - State { - name: "mobile" - PropertyChanges { - target: webEngineView - profile.httpUserAgent: mobileUserAgent - } - }, - State { - name: "desktop" - PropertyChanges { - target: webEngineView - profile.httpUserAgent: desktopUserAgent - } - } - ] + UserAgentGenerator { + id: userAgent + isMobile: Kirigami.Settings.isMobile + } profile { offTheRecord: rootPage.privateMode + httpUserAgent: userAgent.userAgent + onDownloadRequested: { // if we don't accept the request right away, it will be deleted download.accept() // therefore just stop the download again as quickly as possible, // and ask the user for confirmation download.pause() questionLoader.setSource("DownloadQuestion.qml") questionLoader.item.download = download questionLoader.item.visible = true } onDownloadFinished: { if (download.state === WebEngineDownloadItem.DownloadCompleted) { showPassiveNotification(i18n("Download finished")) } else if (download.state === WebEngineDownloadItem.DownloadInterrupted) { showPassiveNotification(i18n("Download failed")) console.log("Download interrupt reason: " + download.interruptReason) } else if (download.state === WebEngineDownloadItem.DownloadCancelled) { console.log("Download cancelled by the user") } } } settings { // Disable builtin error pages in favor of our own errorPageEnabled: false } // Custom context menu Controls.Menu { property var request id: contextMenu Controls.MenuItem { text: i18n("Copy") enabled: (contextMenu.request.editFlags & ContextMenuRequest.CanCopy) != 0 onTriggered: webEngineView.triggerWebAction(WebEngineView.Copy) } Controls.MenuItem { text: i18n("Cut") enabled: (contextMenu.request.editFlags & ContextMenuRequest.CanCut) != 0 onTriggered: webEngineView.triggerWebAction(WebEngineView.Cut) } Controls.MenuItem { text: i18n("Paste") enabled: (contextMenu.request.editFlags & ContextMenuRequest.CanPaste) != 0 onTriggered: webEngineView.triggerWebAction(WebEngineView.Paste) } Controls.MenuItem { enabled: contextMenu.request.linkUrl !== "" text: i18n("Copy Url") onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyLinkToClipboard) } Controls.MenuItem { text: i18n("View source") onTriggered: newTab("view-source:" + webEngineView.url) } Controls.MenuItem { text: i18n("Download") onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadLinkToDisk) } Controls.MenuItem { enabled: contextMenu.request.linkUrl !== "" text: i18n("Open in new Tab") onTriggered: webEngineView.triggerWebAction(WebEngineView.OpenLinkInNewTab) } } focus: true onLoadingChanged: { //print("Loading: " + loading); print(" url: " + loadRequest.url) //print(" icon: " + webEngineView.icon) //print(" title: " + webEngineView.title) /* Handle * - WebEngineView::LoadStartedStatus, * - WebEngineView::LoadStoppedStatus, * - WebEngineView::LoadSucceededStatus and * - WebEngineView::LoadFailedStatus */ var ec = ""; var es = ""; if (loadRequest.status === WebEngineView.LoadSucceededStatus) { if (!rootPage.privateMode) { addHistoryEntry(); } } if (loadRequest.status === WebEngineView.LoadFailedStatus) { print("Load failed: " + loadRequest.errorCode + " " + loadRequest.errorString); ec = loadRequest.errorCode; es = loadRequest.errorString; } errorCode = ec; errorString = es; } Component.onCompleted: { print("WebView completed."); var settings = webEngineView.settings; print("Settings: " + settings); } onIconChanged: { if (icon) browserManager.history.updateIcon(url, icon) } onNewViewRequested: { if (request.userInitiated) { newTab(request.requestedUrl.toString()) showPassiveNotification(i18n("Website was opened in a new tab")) } else { questionLoader.setSource("NewTabQuestion.qml") questionLoader.item.url = request.requestedUrl questionLoader.item.visible = true } } onFullScreenRequested: { request.accept() if (webBrowser.visibility !== Window.FullScreen) webBrowser.showFullScreen() else webBrowser.showNormal() } onContextMenuRequested: { request.accepted = true // Make sure QtWebEngine doesn't show its own context menu. contextMenu.request = request contextMenu.x = request.x contextMenu.y = request.y contextMenu.open() } onAuthenticationDialogRequested: { request.accepted = true sheetLoader.setSource("AuthSheet.qml") sheetLoader.item.request = request sheetLoader.item.open() } onFeaturePermissionRequested: { questionLoader.setSource("PermissionQuestion.qml") questionLoader.item.permission = feature questionLoader.item.origin = securityOrigin questionLoader.item.visible = true } } diff --git a/src/contents/ui/webbrowser.qml b/src/contents/ui/webbrowser.qml index 5970151..486131a 100644 --- a/src/contents/ui/webbrowser.qml +++ b/src/contents/ui/webbrowser.qml @@ -1,359 +1,353 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.1 import QtWebEngine 1.6 import QtQuick.Window 2.3 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.4 as Kirigami import org.kde.mobile.angelfish 1.0 Kirigami.ApplicationWindow { id: webBrowser title: i18n("Angelfish Web Browser") /** Pointer to the currently active view. * * Browser-level functionality should use this to refer to the current * view, rather than looking up views in the mode, as far as possible. */ property Item currentWebView: tabs.currentIndex < tabs.count ? tabs.currentItem : null onCurrentWebViewChanged: { print("Current WebView is now : " + tabs.currentIndex); } property int borderWidth: Math.round(Kirigami.Units.gridUnit / 18); property color borderColor: Kirigami.Theme.highlightColor; /** * Load a url in the current tab */ function load(url) { print("Loading url: " + url); currentWebView.url = url; currentWebView.forceActiveFocus() } BrowserManager { id: browserManager } /** * Make loading available to c++ */ Connections { target: browserManager onLoadUrlRequested: { load(url) } } width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 30 /** * Add page of currently active webview to history */ function addHistoryEntry() { //print("Adding history"); var request = new Object;// FIXME request.url = currentWebView.url; request.title = currentWebView.title; request.icon = currentWebView.icon; request.lastVisited = new Date(); browserManager.addToHistory(request); } // Only show ToolBar if a layer is open property bool layerShown : pageStack.layers.depth > 1 pageStack.globalToolBar.style: layerShown ? Kirigami.ApplicationHeaderStyle.Auto : Kirigami.ApplicationHeaderStyle.None globalDrawer: Kirigami.GlobalDrawer { id: globalDrawer handleVisible: false actions: [ Kirigami.Action { icon.name: "tab-duplicate" onTriggered: { pageStack.layers.push("Tabs.qml") } text: i18n("Tabs") }, Kirigami.Action { icon.name: "view-private" onTriggered: { rootPage.privateMode ? rootPage.privateMode = false : rootPage.privateMode = true } text: rootPage.privateMode ? i18n("Leave private mode") : i18n("Private mode") }, Kirigami.Action { icon.name: "bookmarks" onTriggered: { pageStack.layers.push("Bookmarks.qml") } text: i18n("Bookmarks") }, Kirigami.Action { icon.name: "view-history" onTriggered: { pageStack.layers.push("History.qml") } text: i18n("History") }, Kirigami.Action { icon.name: "configure" text: i18n("Settings") onTriggered: { pageStack.layers.push("Settings.qml") } } ] } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer handleVisible: false } // Main Page pageStack.initialPage: Kirigami.Page { id: rootPage leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 property bool privateMode: false ListWebView { id: tabs anchors { top: parent.top left: parent.left right: parent.right bottom: navigation.top } } ErrorHandler { id: errorHandler errorString: currentWebView.errorString errorCode: currentWebView.errorCode anchors { top: parent.top left: parent.left right: parent.right bottom: navigation.top } visible: currentWebView.errorCode !== "" } Loader { id: questionLoader anchors.bottom: navigation.top anchors.left: parent.left anchors.right: parent.right } // Container for the progress bar Item { id: progressItem height: Math.round(Kirigami.Units.gridUnit / 6) z: navigation.z + 1 anchors { top: tabs.bottom topMargin: -Math.round(height / 2) left: tabs.left right: tabs.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 } // The menu at the bottom right contextualActions: [ Kirigami.Action { icon.name: "edit-find" onTriggered: { sheetLoader.setSource("FindInPageSheet.qml") sheetLoader.item.open() } text: i18n("Find in page") }, Kirigami.Action { icon.name: "document-share" text: i18n("Share page") onTriggered: { sheetLoader.setSource("ShareSheet.qml") sheetLoader.item.url = currentWebView.url sheetLoader.item.title = currentWebView.title sheetLoader.item.open() } }, Kirigami.Action { enabled: currentWebView.canGoBack icon.name: "go-previous" text: i18n("Go previous") onTriggered: { currentWebView.goBack() } }, Kirigami.Action { enabled: currentWebView.canGoForward icon.name: "go-next" text: i18n("Go forward") onTriggered: { currentWebView.goForward() } }, Kirigami.Action { icon.name: currentWebView.loading ? "process-stop" : "view-refresh" text: currentWebView.loading ? i18n("Stop loading") : i18n("Refresh") onTriggered: { currentWebView.loading ? currentWebView.stop() : currentWebView.reload() } }, Kirigami.Action { icon.name: "bookmarks" text: i18n("Add bookmark") onTriggered: { print("Adding bookmark"); var request = new Object;// FIXME request.url = currentWebView.url; request.title = currentWebView.title; request.icon = currentWebView.icon; request.bookmarked = true; browserManager.addBookmark(request); } }, Kirigami.Action { icon.name: "computer" text: i18n("Show desktop site") checkable: true - checked: { - if (currentWebView.state === "mobile") { - false - } else if (currentWebView.state === "desktop") { - true - } - } + checked: !currentWebView.userAgent.isMobile onTriggered: { - if (currentWebView.state === "desktop") { - currentWebView.state = "mobile" - } else if (currentWebView.state === "mobile") { - currentWebView.state = "desktop" + if (currentWebView.userAgent.isMobile) { + currentWebView.userAgent.isMobile = false + } else { + currentWebView.userAgent.isMobile = true } currentWebView.reload() } } ] // Bottom navigation bar Navigation { id: navigation navigationShown: !webappcontainer && webBrowser.visibility !== Window.FullScreen Kirigami.Theme.colorSet: rootPage.privateMode ? Kirigami.Theme.Complementary : Kirigami.Theme.Window layer.enabled: navigation.visible layer.effect: DropShadow { verticalOffset: - 1 color: Kirigami.Theme.disabledTextColor samples: 10 spread: 0.1 cached: true // element is static } anchors { bottom: completion.top left: parent.left right: parent.right } onTextChanged: urlFilter.setFilterFixedString(text) } Completion { id: completion model: UrlFilterProxyModel { id: urlFilter sourceModel: browserManager.history } width: parent.width height: 0.5 * parent.height visible: navigation.textFocus searchText: navigation.text Behavior on height { NumberAnimation { duration: Kirigami.Units.shortDuration * 2 } } anchors { bottom: parent.bottom horizontalCenter: navigation.horizontalCenter } } // Thin line above navigation Rectangle { height: webBrowser.borderWidth color: webBrowser.borderColor anchors { left: parent.left bottom: navigation.top right: parent.right } } } } diff --git a/src/main.cpp b/src/main.cpp index e272a87..b8176fe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,76 +1,79 @@ /* Copyright (C) 2019 Jonah Brüchert This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "browsermanager.h" #include "urlfilterproxymodel.h" #include "urlmodel.h" +#include "useragent.h" Q_DECL_EXPORT int main(int argc, char *argv[]) { QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); QCoreApplication::setOrganizationName("KDE"); QCoreApplication::setOrganizationDomain("mobile.kde.org"); QCoreApplication::setApplicationName("angelfish"); // Command line parser QCommandLineParser parser; parser.addPositionalArgument("url", i18n("URL to open"), "[url]"); parser.addOption({ "webapp-container", i18n("Start without UI") }); parser.addHelpOption(); parser.process(app); // QML loading QQmlApplicationEngine engine; // Setup QtWebEngine qputenv("QTWEBENGINE_DIALOG_SET", "QtQuickControls2"); QtWebEngine::initialize(); // initial url command line parameter QString initialUrl; if (!parser.positionalArguments().isEmpty()) initialUrl = QUrl::fromUserInput(parser.positionalArguments()[0].toUtf8()).toEncoded(); engine.rootContext()->setContextProperty("initialUrl", initialUrl); engine.rootContext()->setContextProperty("webappcontainer", parser.isSet("webapp-container")); // Browser manager qmlRegisterType("org.kde.mobile.angelfish", 1, 0, - "BrowserManager"); + "BrowserManager"); qmlRegisterType("org.kde.mobile.angelfish", 1, 0, - "UrlFilterProxyModel"); + "UrlFilterProxyModel"); + qmlRegisterType("org.kde.mobile.angelfish", 1, 0, + "UserAgentGenerator"); engine.load(QUrl(QStringLiteral("qrc:///webbrowser.qml"))); // Error handling if (engine.rootObjects().isEmpty()) { return -1; } return app.exec(); } diff --git a/src/useragent.cpp b/src/useragent.cpp new file mode 100644 index 0000000..8fc3929 --- /dev/null +++ b/src/useragent.cpp @@ -0,0 +1,32 @@ +#include "useragent.h" + +#include +#include + +UserAgent::UserAgent(QObject *parent) : QObject(parent) +{ + +} + +QString UserAgent::userAgent() const +{ + return QString("Mozilla/5.0 (%1) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/%2 Chrome/75.0.3770.116 %3 Safari/537.36") + .arg(m_isMobile ? QStringLiteral("Linux; Plasma Mobile, like Android 9.0") : QStringLiteral("X11; Linux x86_64")) + .arg(QTWEBENGINE_VERSION_STR) + .arg(m_isMobile ? QStringLiteral("Mobile") : QStringLiteral("Desktop")); +} + +bool UserAgent::isMobile() const +{ + return m_isMobile; +} + +void UserAgent::setIsMobile(bool value) +{ + if (m_isMobile != value) { + m_isMobile = value; + + emit isMobileChanged(); + emit userAgentChanged(); + } +} diff --git a/src/useragent.h b/src/useragent.h new file mode 100644 index 0000000..694a116 --- /dev/null +++ b/src/useragent.h @@ -0,0 +1,29 @@ +#ifndef USERAGENT_H +#define USERAGENT_H + +#include + +class UserAgent : public QObject +{ + Q_PROPERTY(QString userAgent READ userAgent NOTIFY userAgentChanged) + Q_PROPERTY(bool isMobile READ isMobile WRITE setIsMobile NOTIFY isMobileChanged) + + Q_OBJECT + +public: + explicit UserAgent(QObject *parent = nullptr); + + QString userAgent() const; + + bool isMobile() const; + void setIsMobile(bool value); + +signals: + void isMobileChanged(); + void userAgentChanged(); + +private: + int m_isMobile; +}; + +#endif // USERAGENT_H