diff --git a/CMakeLists.txt b/CMakeLists.txt index 78bc6556099a4992dbf2eb2552382e83bc0500a6..ee46ba5baf069e71d4c66c1a0155cd24472a5a0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ include(KDECompilerSettings NO_POLICY_SCOPE) ################# Find dependencies ################# find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2 Sql) -find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 Purpose I18n) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 Purpose I18n Config CoreAddons) # Necessary to support QtWebEngine installed in a different prefix than the rest of Qt (e.g flatpak) find_package(Qt5WebEngine REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 23045aaf64c604d0f81e2997fe5a94660d899519..d8662398af015cea6836136afaacbf66b6f6bde0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,16 @@ set(angelfish_webapp_SRCS qt5_add_resources(WEBAPP_RESOURCES webapp-resources.qrc) add_executable(angelfish-webapp ${angelfish_webapp_SRCS} ${RESOURCES} ${WEBAPP_RESOURCES}) -target_link_libraries(angelfish-webapp Qt5::Core Qt5::Qml Qt5::Quick Qt5::Sql Qt5::Svg Qt5::WebEngine KF5::I18n) +target_link_libraries(angelfish-webapp + Qt5::Core + Qt5::Qml + Qt5::Quick + Qt5::Sql + Qt5::Svg + Qt5::WebEngine + KF5::I18n + KF5::CoreAddons + KF5::ConfigCore + KF5::ConfigGui) install(TARGETS angelfish-webapp ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/contents/webapp-ui/WebAppView.qml b/src/contents/webapp-ui/WebAppView.qml new file mode 100644 index 0000000000000000000000000000000000000000..a51e7865879251a2045dd8aebbc69d9e39bff94b --- /dev/null +++ b/src/contents/webapp-ui/WebAppView.qml @@ -0,0 +1,270 @@ +/*************************************************************************** + * * + * 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: "" + + property bool privateMode: false + + property alias userAgent: 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 + + property bool reloadOnVisible: true + + // 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 + + UserAgentGenerator { + id: userAgent + onUserAgentChanged: webEngineView.reload() + } + + profile { + offTheRecord: 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 { + autoLoadImages: webBrowser.settings.webAutoLoadImages + javascriptEnabled: webBrowser.settings.webJavascriptEnabled + // Disable builtin error pages in favor of our own + errorPageEnabled: false + // Load larger touch icons + touchIconsEnabled: true + // Disable scrollbars on mobile + showScrollBars: !Kirigami.Settings.isMobile + } + + // Custom context menu + Controls.Menu { + property ContextMenuRequest request + id: contextMenu + + Controls.MenuItem { + enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanCopy) != 0 + text: i18n("Copy") + onTriggered: webEngineView.triggerWebAction(WebEngineView.Copy) + } + Controls.MenuItem { + enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanCut) != 0 + text: i18n("Cut") + onTriggered: webEngineView.triggerWebAction(WebEngineView.Cut) + } + Controls.MenuItem { + enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanPaste) != 0 + text: i18n("Paste") + onTriggered: webEngineView.triggerWebAction(WebEngineView.Paste) + } + Controls.MenuItem { + enabled: contextMenu.request != null && contextMenu.request.selectedText + text: contextMenu.request && contextMenu.request.selectedText ? i18n("Search online for '%1'", contextMenu.request.selectedText) : i18n("Search online") + onTriggered: Qt.openUrlExternally(UrlUtils.urlFromUserInput(BrowserManager.searchBaseUrl + contextMenu.request.selectedText)); + } + Controls.MenuItem { + enabled: contextMenu.request !== null && contextMenu.request.linkUrl !== "" + text: i18n("Copy Url") + onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyLinkToClipboard) + } + Controls.MenuItem { + text: i18n("Download") + onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadLinkToDisk) + } + } + + 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.LoadStartedStatus) { + loadingActive = true; + } + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { + if (!privateMode) { + var request = new Object;// FIXME + request.url = currentWebView.url; + request.title = currentWebView.title; + request.icon = currentWebView.icon; + BrowserManager.addToHistory(request); + BrowserManager.updateLastVisited(currentWebView.url); + } + 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; + 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; + errorString = es; + } + + Component.onCompleted: { + print("WebView completed."); + var settings = webEngineView.settings; + print("Settings: " + settings); + } + + onIconChanged: { + if (icon && !privateMode) + BrowserManager.updateIcon(url, icon) + } + + onNewViewRequested: { + if (UrlUtils.urlHost(request.requestedUrl) === UrlUtils.urlHost( BrowserManager.initialUrl)) { + url = request.requestedUrl; + } else { + Qt.openUrlExternally(request.requestedUrl); + } + } + + onUrlChanged: { + if (requestedUrl !== url) { + requestedUrl = url; + } + } + + 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 + } + + onJavaScriptDialogRequested: { + request.accepted = true + sheetLoader.setSource("JavaScriptDialogSheet.qml") + sheetLoader.item.request = request + sheetLoader.item.open() + } + + onVisibleChanged: { + // set user agent to the current displayed tab + // this ensures that we follow mobile preference + // of the current webview. also update the current + // snapshot image with short delay to be sure that + // all kirigami pages have moved into place + if (visible) { + profile.httpUserAgent = Qt.binding(function() { return userAgent.userAgent; }); + if (reloadOnVisible) { + reloadOnVisible = false; + reload(); + } + } + } + + function stopLoading() { + loadingActive = false; + stop(); + } +} diff --git a/src/contents/webapp-ui/webapp.qml b/src/contents/webapp-ui/webapp.qml index 3b5f74fc49bdd6050e1f4f25b1b2097b9d0456f5..41cc0f155ccd19f8fd5d4a413376be45f0d061ad 100644 --- a/src/contents/webapp-ui/webapp.qml +++ b/src/contents/webapp-ui/webapp.qml @@ -73,7 +73,8 @@ Kirigami.ApplicationWindow { // tabs will work correctly property bool initialized: false - WebView { + //FIXME: WebView assumes a multi tab ui, will probably need own implementation + WebAppView { id: webView anchors.fill: parent url: BrowserManager.initialUrl diff --git a/src/urlutils.cpp b/src/urlutils.cpp index 954f529c0780724c42c579428ba55e219273293a..64a355387c54d1b4075f7292cbf42a575c9d01f0 100644 --- a/src/urlutils.cpp +++ b/src/urlutils.cpp @@ -72,3 +72,8 @@ QString UrlUtils::urlPath(const QString &url) { return QUrl::fromUserInput(url).path(); } + +QString UrlUtils::urlHost(const QString &url) +{ + return QUrl::fromUserInput(url).host(); +} diff --git a/src/urlutils.h b/src/urlutils.h index ef7895257766ee2c554ce40b33b7748c397bf235..f404d523ac0438bbfef06a9c36f511812c054f92 100644 --- a/src/urlutils.h +++ b/src/urlutils.h @@ -41,6 +41,7 @@ public: Q_INVOKABLE static QString urlScheme(const QString &url); Q_INVOKABLE static QString urlHostPort(const QString &url); Q_INVOKABLE static QString urlPath(const QString &url); + Q_INVOKABLE static QString urlHost(const QString &url); }; #endif // URLUTILS_H diff --git a/src/webapp-resources.qrc b/src/webapp-resources.qrc index aeaa78f7d6cf738e1250076bc39bb0969b04ec43..76d4d486728c85a5f9078fc139cc0d9369f65096 100644 --- a/src/webapp-resources.qrc +++ b/src/webapp-resources.qrc @@ -1,5 +1,6 @@ contents/webapp-ui/webapp.qml + contents/webapp-ui/WebAppView.qml diff --git a/src/webappmain.cpp b/src/webappmain.cpp index f1d16cd89b28e1e39aa5dbf077c0dddcebb78763..2a1a134aba4ccedae8e5fafbaf9769f47a42b807 100644 --- a/src/webappmain.cpp +++ b/src/webappmain.cpp @@ -25,6 +25,8 @@ #include #include +#include +#include #include "bookmarkshistorymodel.h" #include "browsermanager.h" @@ -45,9 +47,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) #endif QApplication app(argc, argv); - QCoreApplication::setOrganizationName("KDE"); - QCoreApplication::setOrganizationDomain("mobile.kde.org"); - QCoreApplication::setApplicationName("angelfish"); + //QCoreApplication::setOrganizationName("KDE"); + //QCoreApplication::setOrganizationDomain("mobile.kde.org"); + //QCoreApplication::setApplicationName("angelfish"); #if QT_VERSION <= QT_VERSION_CHECK(5, 14, 0) QtWebEngine::initialize(); @@ -65,10 +67,27 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) engine.addImageProvider(IconImageProvider::providerId(), new IconImageProvider(&engine)); - // initial url command line parameter - QString initialUrl; - if (!parser.positionalArguments().isEmpty()) - initialUrl = QUrl::fromUserInput(parser.positionalArguments().first()).toString(); + if (parser.positionalArguments().isEmpty()) { + return 0; + } + + const QString fileName = parser.positionalArguments().first(); + KDesktopFile desktopFile(fileName); + if (desktopFile.readUrl().isEmpty()) { + return 0; + } + const QString initialUrl = QUrl::fromUserInput(desktopFile.readUrl()).toString(); + + const QString appName = desktopFile.readName().toLower().replace(QLatin1Char(' '), QLatin1Char('-')) + QLatin1String("-angelfish-webapp"); + KAboutData aboutData(appName.toLower(), desktopFile.readName(), + QStringLiteral("0.1"), + i18n("Angelfish Web App runtime"), + KAboutLicense::GPL, + i18n("Copyright 2020 Angelfish developers")); + QApplication::setWindowIcon(QIcon::fromTheme(desktopFile.readIcon())); + aboutData.addAuthor(i18n("Marco Martin"), QString(), "mart@kde.org"); + + KAboutData::setApplicationData(aboutData); // Exported types qmlRegisterType("org.kde.mobile.angelfish", 1, 0, "BookmarksHistoryModel");