diff --git a/mobile/scripts/buildApp.sh b/mobile/scripts/buildApp.sh index 0a988d81d1..ff3aeb9453 100755 --- a/mobile/scripts/buildApp.sh +++ b/mobile/scripts/buildApp.sh @@ -38,6 +38,12 @@ fi if [[ "${OS}" == "android" ]]; then [[ -z "${JAVA_HOME}" ]] && { echo "JAVA_HOME is not set"; exit 1; } + STATUSQ_DIR=${STATUSQ:-"${STATUS_DESKTOP}/ui/StatusQ"} + MOBILEWEBVIEW_JAVA_DIR_FILE="${STATUSQ_DIR}/build/${OS}/StatusQ/mobilewebview_java_dir.txt" + if [[ -z "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}" && -f "${MOBILEWEBVIEW_JAVA_DIR_FILE}" ]]; then + MOBILEWEBVIEW_ANDROID_JAVA_SRC="$(<"${MOBILEWEBVIEW_JAVA_DIR_FILE}")" + fi + # Export BUILD_VARIANT for build.gradle to pick up export BUILD_VARIANT @@ -62,6 +68,12 @@ if [[ "${OS}" == "android" ]]; then rsync -a --exclude='libs.xml' "$CWD/../android/qt${QT_MAJOR}/res/" "$BUILD_DIR/android-build/res/" 2>/dev/null || true rsync -a "$CWD/../android/qt${QT_MAJOR}/src/" "$BUILD_DIR/android-build/src/" 2>/dev/null || true + if [[ -n "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}" && \ + -f "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}/org/mobilewebview/MobileWebView.java" ]]; then + echo "Including MobileWebView Java sources from: ${MOBILEWEBVIEW_ANDROID_JAVA_SRC}" + rsync -a "${MOBILEWEBVIEW_ANDROID_JAVA_SRC}/org/" "$BUILD_DIR/android-build/src/org/" + fi + cd "$BUILD_DIR/android-build" BIN_DIR=${BIN_DIR:-"$CWD/../bin/android/qt6"} diff --git a/mobile/scripts/buildStatusQ.sh b/mobile/scripts/buildStatusQ.sh index cf717f4758..5177fd5c53 100755 --- a/mobile/scripts/buildStatusQ.sh +++ b/mobile/scripts/buildStatusQ.sh @@ -34,6 +34,12 @@ cmake -S "${STATUSQ}" -B "${BUILD_DIR}" \ make -C "${BUILD_DIR}" SCodes -j "$(nproc)" make -C "${BUILD_DIR}" StatusQ -j "$(nproc)" +MOBILEWEBVIEW_JAVA_DIR_FILE="${BUILD_DIR}/mobilewebview_java_dir.txt" +if [[ -f "${MOBILEWEBVIEW_JAVA_DIR_FILE}" ]]; then + export MOBILEWEBVIEW_ANDROID_JAVA_SRC + MOBILEWEBVIEW_ANDROID_JAVA_SRC="$(<"${MOBILEWEBVIEW_JAVA_DIR_FILE}")" +fi + mkdir -p "${LIB_DIR}" STATUSQ_LIB=$(find "${BUILD_DIR}" -name "libStatusQ${LIB_SUFFIX}${LIB_EXT}") diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim index 86ce974695..66b71a30b5 100644 --- a/src/nim_status_client.nim +++ b/src/nim_status_client.nim @@ -68,6 +68,16 @@ proc determineStatusAppIconPath(): string = return "/../status-dev.png" +proc ensureQmlSelector(selector: string) = + let current = getEnv("QT_FILE_SELECTORS") + if current.len == 0: + putEnv("QT_FILE_SELECTORS", selector) + return + + let selectors = current.split(",") + if selector notin selectors: + putEnv("QT_FILE_SELECTORS", current & "," & selector) + proc prepareLogging() = # Outputs logs in the node tab for output in defaultChroniclesStream.outputs.fields(): @@ -180,6 +190,10 @@ proc mainProc() = ensureDirectories(DATADIR, TMPDIR, LOGDIR) + # Mobile builds use the mobile selector for browser WebView adapter replacement. + when main_constants.IS_MOBILE: + ensureQmlSelector("mobile") + let isExperimental = isExperimental() let resourcesPath = determineResourcePath() let openUri = determineOpenUri() diff --git a/storybook/pages/BrowserLayoutPage.qml b/storybook/pages/BrowserLayoutPage.qml index 49d5569e8a..a538175be6 100644 --- a/storybook/pages/BrowserLayoutPage.qml +++ b/storybook/pages/BrowserLayoutPage.qml @@ -33,6 +33,7 @@ SplitView { userUID: "0xdeadbeef" transactionStore: TransactionStoreMock {} thirdpartyServicesEnabled: true + dappsEnabled: true connectorController: null platformOS: ctrlPlatformOS.currentValue diff --git a/storybook/validator_main.cpp b/storybook/validator_main.cpp index 6ea00a0431..48199d622e 100644 --- a/storybook/validator_main.cpp +++ b/storybook/validator_main.cpp @@ -13,10 +13,6 @@ int main(int argc, char *argv[]) QGuiApplication app(argc, argv); - // FIXME: revert when full integration is done - // (https://github.com/status-im/status-app/pull/20178) - qputenv("QT_FILE_SELECTORS", "noWebEngine"); - QGuiApplication::setOrganizationName(u"Status"_s); QGuiApplication::setOrganizationDomain(u"status.im"_s); diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index c4dd9e8b5a..3c0c2ba8a1 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -294,7 +294,7 @@ if(IOS OR ANDROID OR CMAKE_SYSTEM_NAME MATCHES "Darwin") MobileWebView GIT_REPOSITORY https://github.com/status-im/mobilewebview.git - GIT_TAG 551b1610deceec99b5b99e9612ab4947faa8c332 + GIT_TAG feb2db5a5374ef75c1c11162b6892189c199f954 SOURCE_SUBDIR mobilewebview ) FetchContent_MakeAvailable(MobileWebView) @@ -302,6 +302,10 @@ if(IOS OR ANDROID OR CMAKE_SYSTEM_NAME MATCHES "Darwin") if(TARGET MobileWebView) target_link_libraries(StatusQ PRIVATE MobileWebView) target_compile_definitions(StatusQ PRIVATE STATUSQ_HAS_MOBILEWEBVIEW=1) + if(ANDROID AND DEFINED mobilewebview_SOURCE_DIR) + file(WRITE "${CMAKE_BINARY_DIR}/mobilewebview_java_dir.txt" + "${mobilewebview_SOURCE_DIR}/mobilewebview/android/src") + endif() install(TARGETS MobileWebView RUNTIME DESTINATION StatusQ LIBRARY DESTINATION StatusQ diff --git a/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml b/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml deleted file mode 100644 index 6aba0d5647..0000000000 --- a/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml +++ /dev/null @@ -1,102 +0,0 @@ -import QtQuick - -import QtWebEngine -import QtWebChannel - -import StatusQ - -// Helper to load and setup an instance of \c WebEngineView -// -// The \c webChannelObjects property is used to register specific objects -// -// Loading qrc:/StatusQ/Components/private/qwebchannel/qwebchannel.js and -// qrc:/StatusQ/Components/private/qwebchannel/helpers.js will provide -// access to window.statusq APIs used to exchange data between the internal -// web engine and the QML application -// -// It doesn't load the web engine until NetworkChecker detects and active internet -// connection to avoid the corner case of initializing the web engine without -// network connectivity. If the web engine is initialized without network connectivity -// it won't restore the connectivity when it's available on Mac OS -Item { - id: root - - required property url url - required property var webChannelObjects - property string profileName: "Default" - - // Used to control the loading of the web engine - property bool active: false - // Useful to monitor the loading state of the web engine (depends on active and internet connectivity) - readonly property bool isActive: loader.active - property alias instance: loader.item - property bool waitForInternet: true - - signal engineLoaded(WebEngineView instance) - signal engineUnloaded() - signal pageLoaded() - signal pageLoadingError(string errorString) - - Component { - id: webEngineViewComponent - - WebEngineView { - id: webEngineView - - anchors.fill: parent - - visible: false - - url: root.url - webChannel: statusChannel - profile.storageName: root.profileName - - onLoadingChanged: function(loadRequest) { - switch(loadRequest.status) { - case WebEngineView.LoadSucceededStatus: - root.pageLoaded() - break - case WebEngineView.LoadFailedStatus: - root.pageLoadingError(loadRequest.errorString) - break - } - } - - WebChannel { - id: statusChannel - registeredObjects: root.webChannelObjects - } - } - } - - Loader { - id: loader - - active: root.active && (!root.waitForInternet || (d.passedFirstTimeInitialization || networkChecker.isOnline)) - - onStatusChanged: function() { - if (status === Loader.Ready) { - root.engineLoaded(loader.item) - d.passedFirstTimeInitialization = true - } else if (status === Loader.Null) { - root.engineUnloaded() - } - } - - sourceComponent: webEngineViewComponent - } - - NetworkChecker { - id: networkChecker - - // Deactivate searching for network connectivity after the web engine is loaded - active: !d.passedFirstTimeInitialization - } - - QtObject { - id: d - - // Used to hold the loading of the web engine until internet connectivity is available - property bool passedFirstTimeInitialization: false - } -} \ No newline at end of file diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index 99004e6477..f78e9bd25d 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -68,5 +68,4 @@ StatusToastMessage 0.1 StatusToastMessage.qml StatusToolBar 0.1 StatusToolBar.qml StatusUserImage 0.1 StatusUserImage.qml StatusVideo 0.1 StatusVideo.qml -WebEngineLoader 0.1 WebEngineLoader.qml StatusLoadingPageIndicator 0.1 StatusLoadingPageIndicator.qml diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml b/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml index 7f56943a45..bb6c8146e2 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml +++ b/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml @@ -332,4 +332,17 @@ QtObject { return content.length > 0 ? (content + tidSuffix) : tidSuffix } + + function hasPopups(overlayChildren) { + if (!overlayChildren) + return false + + return overlayChildren.filter( + item => { + if (!item) + return false + const str = item.toString() + return str.includes("QQuickPopupItem") && !str.includes("StatusToolTip") + }).length > 0 + } } diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 065d621984..6105068fb7 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -67,7 +67,6 @@ StatusQ/Components/StatusToolBar.qml StatusQ/Components/StatusUserImage.qml StatusQ/Components/StatusVideo.qml - StatusQ/Components/WebEngineLoader.qml StatusQ/Components/private/LoadingDotItem.qml StatusQ/Components/private/StatusComboboxBackground.qml StatusQ/Components/private/StatusComboboxIndicator.qml diff --git a/ui/StatusQ/tests/TestComponents/WebEngineLoader/test.html b/ui/StatusQ/tests/TestComponents/WebEngineLoader/test.html deleted file mode 100644 index 9a13c3b6b1..0000000000 --- a/ui/StatusQ/tests/TestComponents/WebEngineLoader/test.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Test Page - - - - -

Test Page

- - diff --git a/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml b/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml deleted file mode 100644 index cad04cab4d..0000000000 --- a/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml +++ /dev/null @@ -1,102 +0,0 @@ -import QtQuick -import QtTest - -import QtWebEngine -import QtWebChannel - -import StatusQ.Components -import StatusQ.TestHelpers - -TestCase { - id: root - name: "TestWebEngineLoader" - - QtObject { - id: testObject - - WebChannel.id: "testObject" - - signal webChannelInitOk() - signal webChannelError() - function signalWebChannelInitResult(error) { - if(error) { - webChannelError() - } else { - webChannelInitOk() - } - } - } - - Loader { - id: loader - - active: false - - sourceComponent: WebEngineLoader { - url: Qt.resolvedUrl("./WebEngineLoader/test.html") - webChannelObjects: [testObject] - - waitForInternet: false - } - } - SignalSpy { id: loadedSpy; target: loader; signalName: "loaded" } - - SignalSpy { id: webEngineLoadedSpy; target: loader.item; signalName: "engineLoaded" } - SignalSpy { id: pageLoadedSpy; target: loader.item; signalName: "pageLoaded" } - SignalSpy { id: engineUnloadedSpy; target: loader.item; signalName: "engineUnloaded" } - SignalSpy { id: pageLoadingErrorSpy; target: loader.item; signalName: "onPageLoadingError" } - - function init() { - for (var i = 0; i < root.children.length; i++) { - const child = root.children[i] - if(child.hasOwnProperty("signalName")) { - child.clear() - } - } - loader.active = true - loadedSpy.wait(1000); - } - - function cleanup() { - loader.active = false - } - - function test_loadUnload() { - const webEngine = loader.item - compare(webEngine.instance, null, "By default the engine is not loaded") - webEngine.active = true - - webEngineLoadedSpy.wait(1000) - verify(webEngine.instance !== null , "The WebEngineView should be available") - - pageLoadedSpy.wait(1000) - webEngine.active = false - engineUnloadedSpy.wait(1000); - - verify(webEngine.instance === null , "The WebEngineView should be unavailable") - } - - SignalSpy { id: wcInitOkSpy; target: testObject; signalName: "webChannelInitOk" } - SignalSpy { id: wcInitErrorSpy; target: testObject; signalName: "webChannelError" } - function test_executeCode() { - const webEngine = loader.item - webEngine.active = true - pageLoadedSpy.wait(1000); - - let errorResult = null - webEngine.instance.runJavaScript(` - window.testError = window.statusq.error; - try { - window.statusq.channel.objects.testObject.signalWebChannelInitResult(""); - } catch (e) { - window.testError = e.message; - } - window.testError - `, function(result) { - errorResult = result - }) - - wcInitOkSpy.wait(1000); - compare(errorResult, "", "Expected empty error string if all good") - } -} diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index d03758e724..b1de3938b4 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtWebEngine import QtModelsToolkit @@ -26,6 +25,7 @@ import AppLayouts.Browser.popups import AppLayouts.Browser.controls import AppLayouts.Browser.views import AppLayouts.Browser.panels +import AppLayouts.Browser.webview // Code based on https://code.qt.io/cgit/qt/qtwebengine.git/tree/examples/webengine/quicknanobrowser/BrowserWindow.qml?h=5.15 // Licensed under BSD @@ -35,6 +35,7 @@ StatusSectionLayout { required property bool isMobile required property string userUID required property bool thirdpartyServicesEnabled + required property bool dappsEnabled required property TransactionStore transactionStore @@ -49,23 +50,20 @@ StatusSectionLayout { property bool isDebugEnabled: false property string platformOS: Qt.platform.os - // FIXME: restore in the next PR - property bool useWebViewAdapter: false - readonly property Component webViewAdapterComponent: webEngineAdapterComponent signal sendToRecipientRequested(string address) function openUrlInNewTab(url) { var tab = _internal.addNewTab() - tab.url = _internal.determineRealURL(url) + tab.url = root.browserRootStore.determineRealURL(url) } function reloadCurrentTab() { - _internal.currentWebView?.reload() + webViewContext.reloadCurrent() } Component.onCompleted: { - var tab = webStackView.createEmptyTab(connectorBridge.defaultProfileParams, true); + var tab = webViewContext.createEmptyTab(connectorBridge.defaultProfileParams, true); // For Devs: Uncomment the next line if you want to use the simpledapp on first load // tab.url = root.browserRootStore.determineRealURL("https://simpledapp.eth"); } @@ -96,91 +94,25 @@ StatusSectionLayout { QtObject { id: _internal - property Item currentWebView: tabs.currentIndex < tabs.count ? webStackView.getCurrentWebView() : null - readonly property bool currentTabIncognito: webStackView.getCurrentWebView()?.offTheRecord ?? false + property Item currentWebView: webViewContext.currentWebView + readonly property bool currentTabIncognito: currentWebView?.offTheRecord ?? false property bool webViewHidden: false property Component jsDialogComponent: JSDialogWindow {} - property Component accessDialogComponent: BrowserConnectionModal { - browserRootStore: root.browserRootStore - browserWalletStore: root.browserWalletStore - - parent: mainView - x: mainView.width - width - Theme.halfPadding - y: mainView.y + browserToolbar.height + Theme.halfPadding - } - - property Component sendTransactionModalComponent: SendModal { - anchors.centerIn: parent - preSelectedHoldingID: "ETH" - preSelectedHoldingType: Constants.TokenType.ERC20 - store: root.transactionStore - } - - property Component signMessageModalComponent: SignMessageModal { - browserRootStore: root.browserRootStore - signingPhrase: root.browserWalletStore.signingPhrase - } - - property StatusMessageDialog sendingError: StatusMessageDialog { - title: qsTr("Error sending the transaction") - icon: StatusMessageDialog.StandardIcon.Critical - standardButtons: Dialog.Ok - } - - property StatusMessageDialog signingError: StatusMessageDialog { - title: qsTr("Error signing message") - icon: StatusMessageDialog.StandardIcon.Critical - standardButtons: Dialog.Ok - } - function addNewDownloadTab() { - webStackView.createDownloadTab(tabs.count !== 0 ? currentWebView.profileParams : connectorBridge.defaultProfileParams); + webViewContext.createDownloadTab(tabs.count !== 0 ? currentWebView.profileParams : connectorBridge.defaultProfileParams); tabs.currentIndex = tabs.count - 1; } function addNewTab() { - var tab = webStackView.createEmptyTab(tabs.count !== 0 ? currentWebView.profileParams : connectorBridge.defaultProfileParams); + var tab = webViewContext.createEmptyTab(tabs.count !== 0 ? currentWebView.profileParams : connectorBridge.defaultProfileParams); browserToolbar.activateAddressBar() return tab; } - function onDownloadRequested(download) { - download.accept(); - root.downloadsStore.addDownload(download) - root.showFooter = true - - // close the tab launched only for starting download - if (!download.view) - return - - // find tab for this view - for (var i = 0; i < tabs.count; ++i) { - var tab = webStackView.getWebView(i) - // close the “download-only” tab - if (tab === download.view && - !tab.htmlPageLoaded && - tab.title === "") { - webStackView.removeView(i) - break - } - } - } - - function determineRealURL(url) { - return root.browserRootStore.determineRealURL(url) - } - onCurrentWebViewChanged: () => findBar.reset() - readonly property var currentViewBookmarkEntry: ModelEntry { - sourceModel: root.bookmarksStore.bookmarksModel - key: "url" - value: (_internal.currentWebView && _internal.currentWebView.url) - ? _internal.currentWebView.url.toString() - : "" - } } invertedLayout: root.isMobile @@ -188,6 +120,34 @@ StatusSectionLayout { headerPadding: 0 backgroundColor: Theme.palette.statusAppNavBar.backgroundColor + BrowserFavoritesContext { + id: favoritesContext + currentWebView: _internal.currentWebView + bookmarksStore: root.bookmarksStore + shouldShowFavoritesBar: localAccountSensitiveSettings.shouldShowFavoritesBar + openPopupFn: (popup, params) => Global.openPopup(popup, params) + addFavoriteModal: addFavoriteModal + } + + BrowserDialogsContext { + id: dialogsContext + networksStore: root.networksStore + browserActivityStore: root.browserActivityStore + browserWalletStore: root.browserWalletStore + openPopupFn: (popup, params) => Global.openPopup(popup, params) + jsDialogComponent: _internal.jsDialogComponent + dialogParent: root + } + + BrowserDownloadsContext { + id: downloadsContext + downloadsStore: root.downloadsStore + tabsModel: tabs + getWebViewFn: (index) => webViewContext.getWebView(index) + removeViewFn: (index) => webViewContext.removeView(index) + setFooterVisibleFn: (visible) => root.showFooter = visible + } + // TODO: move this to a single browser header qml file headerContent: ColumnLayout { spacing: 0 @@ -200,14 +160,14 @@ StatusSectionLayout { currentTabIncognito: _internal.currentTabIncognito determineRealURL: function(url) { - return _internal.determineRealURL(url) + return root.browserRootStore.determineRealURL(url) } onOpenNewTabTriggered: _internal.addNewTab() fnGetWebView: (index) => { - return webStackView.getWebView(index) + return webViewContext.getWebView(index) } onRemoveView: (index) => { - webStackView.removeView(index) + webViewContext.removeView(index) } } @@ -226,87 +186,61 @@ StatusSectionLayout { walletAccountsBtnAvailable: !root.isMobile openTabsCount: tabs.count - currentTabIncognito: _internal.currentWebView?.profile.offTheRecord ?? false - currentTabIsBookmark: _internal.currentViewBookmarkEntry.available &&_internal.currentViewBookmarkEntry.item + currentTabIncognito: _internal.currentWebView?.offTheRecord ?? false + currentTabIsBookmark: favoritesContext.currentTabIsBookmark currentTabLoading: (!!_internal.currentWebView && _internal.currentWebView.loading) browserDappsModel: browserDappsProvider.model - onRequestHistoryPopup: () => historyMenu.open() - onRequestGoBack: () => _internal.currentWebView.goBack() - onRequestGoForward: () => _internal.currentWebView.goForward() - onRequestReloadPage: () => _internal.currentWebView.reload() - onRequestStopLoadingPage: () => _internal.currentWebView.stop() + onRequestHistoryPopup: () => dialogsContext.openHistoryMenu(historyMenu) + onRequestGoBack: () => webViewContext.goBackCurrent() + onRequestGoForward: () => webViewContext.goForwardCurrent() + onRequestReloadPage: () => webViewContext.reloadCurrent() + onRequestStopLoadingPage: () => webViewContext.stopCurrent() onRequestOpenDapp: (url) => { if (_internal.currentWebView) { - _internal.currentWebView.url = _internal.determineRealURL(url) + webViewContext.setCurrentWebUrl(url) } } onRequestDisconnectDapp: (dappUrl) => { connectorBridge.disconnect(dappUrl) } onAddBookmarkRequested: () => { - Global.openPopup(addFavoriteModal, - { - modifiyModal: !!browserToolbar.currentTabIsBookmark, - toolbarMode: true, - ogUrl: _internal.currentViewBookmarkEntry.item && _internal.currentViewBookmarkEntry.available ? - _internal.currentViewBookmarkEntry.item.url : _internal.currentWebView.url, - ogName: _internal.currentViewBookmarkEntry.item && _internal.currentViewBookmarkEntry.available ? - _internal.currentViewBookmarkEntry.item.name : _internal.currentWebView.title - }) + favoritesContext.openAddFavoritePopup(!!browserToolbar.currentTabIsBookmark) } onRequestLaunchInBrowser: (url) => { if (localAccountSensitiveSettings.useBrowserEthereumExplorer !== Constants.browserEthereumExplorerNone && url.startsWith("0x")) { - _internal.currentWebView.url = root.browserRootStore.get0xFormedUrl(localAccountSensitiveSettings.useBrowserEthereumExplorer, url) + webViewContext.setCurrentWebUrl(root.browserRootStore.get0xFormedUrl(localAccountSensitiveSettings.useBrowserEthereumExplorer, url)) return } if (localAccountSensitiveSettings.selectedBrowserSearchEngineId !== SearchEnginesConfig.browserSearchEngineNone && !Utils.isURL(url) && !Utils.isURLWithOptionalProtocol(url)) { - _internal.currentWebView.url = root.browserRootStore.getFormedUrl(localAccountSensitiveSettings.selectedBrowserSearchEngineId, url) + webViewContext.setCurrentWebUrl(root.browserRootStore.getFormedUrl(localAccountSensitiveSettings.selectedBrowserSearchEngineId, url)) return } else if (Utils.isURLWithOptionalProtocol(url)) { url = "https://" + url } - _internal.currentWebView.url = _internal.determineRealURL(url); + webViewContext.setCurrentWebUrl(url); } - onRequestWalletMenu: () => { - // Initialize activity filters before opening popup - const activeChainIds = SQUtils.ModelUtils.modelToFlatArray( - root.networksStore.activeNetworks, "chainId") - if (activeChainIds.length > 0) { - root.browserActivityStore.activityController.setFilterChainsJson( - JSON.stringify(activeChainIds), true) - } - const currentAddress = root.browserWalletStore.dappBrowserAccount.address - root.browserActivityStore.activityController.setFilterAddressesJson( - JSON.stringify([currentAddress])) - - Global.openPopup(browserWalletMenu) - } + onRequestWalletMenu: () => dialogsContext.openWalletMenu(browserWalletMenu) onRequestAllOpenTabsView: () => { // TODO: Launch All Tabs View // https://github.com/status-im/status-app/issues/19569 } - onOpenSettingMenu: () => { - settingsMenu.open() - } + onOpenSettingMenu: () => dialogsContext.openSettingsMenu(settingsMenu) } Loader { id: favoritesBarLoader Layout.fillWidth: true - Layout.preferredHeight: active ? 38: 0 - active: localAccountSensitiveSettings.shouldShowFavoritesBar && - root.bookmarksStore.bookmarksModel.ModelCount.count > 0 + Layout.preferredHeight: favoritesContext.favoritesBarActive ? 38 : 0 + active: favoritesContext.favoritesBarActive sourceComponent: FavoritesBar { currentTabIncognito: _internal.currentTabIncognito bookmarkModel: root.bookmarksStore.bookmarksModel favoritesMenu: favoriteMenu - onSetAsCurrentWebUrl: (url) => _internal.currentWebView.url = _internal.determineRealURL(url) + onSetAsCurrentWebUrl: (url) => webViewContext.setCurrentWebUrl(url) onOpenInNewTab: (url) => root.openUrlInNewTab(url) onAddFavModalRequested: { - Global.openPopup(addFavoriteModal, {toolbarMode: true, - ogUrl: _internal.currentViewBookmarkEntry.item ? _internal.currentViewBookmarkEntry.item.url : _internal.currentWebView.url, - ogName: _internal.currentViewBookmarkEntry.item ? _internal.currentViewBookmarkEntry.item.name : _internal.currentWebView.title}) + favoritesContext.openAddFavoritePopup(false) } } } @@ -323,13 +257,13 @@ StatusSectionLayout { onFindNext: { if (text) - _internal.currentWebView && _internal.currentWebView.findText(text); + webViewContext.findTextCurrent(text) else if (!visible) visible = true; } onFindPrevious: { if (text) - _internal.currentWebView && _internal.currentWebView.findText(text, WebEngineView.FindBackward); + webViewContext.findTextCurrent(text, true) else if (!visible) visible = true; } @@ -340,6 +274,33 @@ StatusSectionLayout { sourceComponent: downloadBar } + BrowserWebViewContext { + id: webViewContext + thirdpartyServicesEnabled: root.thirdpartyServicesEnabled + isDebugEnabled: root.isDebugEnabled + isMobile: root.isMobile + browserSettings: localAccountSensitiveSettings + webChannel: connectorBridge.channel + hostStackLayout: webStackView + tabsModel: tabs + defaultProfileParams: connectorBridge.defaultProfileParams + bookmarksStore: root.bookmarksStore + downloadsStore: root.downloadsStore + determineRealURLFn: (url) => root.browserRootStore.determineRealURL(url) + downloadRequestHandler: (download) => downloadsContext.handleDownloadRequest(download) + sslErrorHandler: (error) => { + error.defer() + sslDialog.enqueue(error) + } + jsDialogHandler: (request) => dialogsContext.openJsDialog(request) + findTextFinishedHandler: (result) => { + if (!findBar.visible) + findBar.visible = true + findBar.numberOfMatches = result.numberOfMatches + findBar.activeMatch = result.activeMatch + } + } + centerPanel: ColumnLayout { id: mainView spacing: 0 @@ -350,54 +311,6 @@ StatusSectionLayout { Layout.fillHeight: true Layout.fillWidth: true - - function createEmptyTab(profileParams, createAsStartPage = false, focusOnNewTab = true, url = undefined) { - focusOnNewTab = focusOnNewTab && !createAsStartPage - - var webview = root.webViewAdapterComponent.createObject(webStackView, { - profileParams: profileParams, - isDownloadView: false - }) - - tabs.createEmptyTab(createAsStartPage, focusOnNewTab, webview) - - if (createAsStartPage && root.thirdpartyServicesEnabled) { - webview.url = Constants.browserDefaultHomepage - } else if (url !== undefined) { - webview.url = url; - } else if (!!localAccountSensitiveSettings.browserHomepage) { - webview.url = _internal.determineRealURL(localAccountSensitiveSettings.browserHomepage) - } - - return webview; - } - - function createDownloadTab(profileParams) { - var webview = root.webViewAdapterComponent.createObject(webStackView, { - profileParams: profileParams, - isDownloadView: true - }) - tabs.createDownloadTab() - return webview; - } - - function getCurrentWebView() { // -> WebEngineView/WebView - return getWebView(tabs.currentIndex) - } - - function getWebView(index) { // -> WebEngineView/WebView - return webStackView.children[index] - } - - function removeView(index) { - if (tabs.count <= 1) { - createEmptyTab(_internal.currentWebView.profileParams, true) - } - tabs.removeTab(index) - var view = getWebView(index) - view.stop() - view.destroy() - } } // Overlay for DownloadView and EmptyWebPage @@ -405,12 +318,13 @@ StatusSectionLayout { id: overlayLoader anchors.fill: parent z: 53 - - readonly property bool showDownloadView: _internal.currentWebView?.isDownloadView ?? false - readonly property bool showEmptyPage: !showDownloadView && (!_internal.currentWebView?.url?.toString()) - - active: showDownloadView || showEmptyPage - sourceComponent: showDownloadView ? downloadViewComponent : emptyPageComponent + + readonly property int contentMode: webViewContext.currentContentMode + + active: contentMode !== BrowserWebViewContext.ContentMode.WebContent + sourceComponent: contentMode === BrowserWebViewContext.ContentMode.DownloadContent + ? downloadViewComponent + : emptyPageComponent } // Non UI component @@ -439,52 +353,13 @@ StatusSectionLayout { } } - Component { - id: webEngineAdapterComponent - WebEngineAdapter { - id: webEngineAdapterItem - anchors.fill: parent - webChannel: connectorBridge.channel - enableJsLogs: root.isDebugEnabled - devToolsEnabled: localAccountSensitiveSettings.devToolsEnabled - - onWindowCloseRequested: webStackView.removeView(StackLayout.index) - onNewWindowRequested: (makeCurrent, requestedUrl, callback) => { - var tab = webStackView.createEmptyTab(_internal.currentWebView.profileParams, false, makeCurrent, requestedUrl); - callback(tab); - } - onDownloadRequested: (download) => { - _internal.onDownloadRequested(download) - } - onCertificateError: (error) => { - error.defer() - sslDialog.enqueue(error) - } - onJavaScriptDialogRequested: (request) => { - request.accepted = true; - var dialog = _internal.jsDialogComponent.createObject(root, {"request": request}) - dialog.open() - } - onFindTextFinished: (result) => { - if (!findBar.visible) - findBar.visible = true - findBar.numberOfMatches = result.numberOfMatches; - findBar.activeMatch = result.activeMatch; - } - } - } - - Component { id: downloadViewComponent DownloadView { downloadsModel: root.downloadsStore.downloadModel downloadsMenu: downloadMenuInst onOpenDownloadClicked: function(downloadComplete, index) { - if (downloadComplete) { - return root.downloadsStore.openFile(index) - } - root.downloadsStore.openDirectory(index) + downloadsContext.openDownloadFromList(downloadComplete, index) } } } @@ -496,11 +371,9 @@ StatusSectionLayout { favMenu: favoriteMenu addFavModal: addFavoriteModal determineRealURLFn: function(url) { - return _internal.determineRealURL(url) + return root.browserRootStore.determineRealURL(url) } - onSetCurrentWebUrl: (url) => { - _internal.currentWebView.url = url - } + onSetCurrentWebUrl: (url) => webViewContext.setCurrentWebUrl(url) Component.onCompleted: { // Add fav button at the end of the grid var index = root.bookmarksStore.getBookmarkIndexByUrl(Constants.newBookmark) @@ -513,6 +386,8 @@ StatusSectionLayout { Component { id: browserWalletMenu BrowserWalletMenu { + id: walletMenu + parent: browserToolbar x: browserToolbar.width - width - Theme.halfPadding y: browserToolbar.height + 4 @@ -528,7 +403,7 @@ StatusSectionLayout { onAccountChanged: (newAddress) => connectorBridge.connectorManager.changeAccount(newAddress) onReload: { for (let i = 0; i < tabs.count; ++i){ - webStackView.getWebView(i).reload(); + webViewContext.getWebView(i).reload(); } } @@ -550,6 +425,7 @@ StatusSectionLayout { root.browserActivityStore.currentActivityFiltersStore.updateRecipientsModel() } } + } } @@ -564,20 +440,10 @@ StatusSectionLayout { zoomFactor: _internal.currentWebView ? _internal.currentWebView.zoomFactor : 1 onAddNewTab: _internal.addNewTab() onAddNewDownloadTab: _internal.addNewDownloadTab() - onGoIncognito: function (checked) { - if (_internal.currentWebView) { - _internal.currentWebView.offTheRecord = checked; - } - } - onZoomIn: { - const newZoom = _internal.currentWebView.zoomFactor + 0.1 - _internal.currentWebView.changeZoomFactor(newZoom) - } - onZoomOut: { - const newZoom = _internal.currentWebView.zoomFactor - 0.1 - _internal.currentWebView.changeZoomFactor(newZoom) - } - onResetZoomFactor: _internal.currentWebView.changeZoomFactor(1.0) + onGoIncognito: (checked) => webViewContext.setIncognitoCurrent(checked) + onZoomIn: webViewContext.changeZoomCurrent(0.1) + onZoomOut: webViewContext.changeZoomCurrent(-0.1) + onResetZoomFactor: webViewContext.resetZoomCurrent() onLaunchFindBar: { if (!findBar.visible) { findBar.visible = true; @@ -586,18 +452,19 @@ StatusSectionLayout { } onToggleCompatibilityMode: function(checked) { for (let i = 0; i < tabs.count; ++i){ - webStackView.getWebView(i).stop() // Stop all loading tabs + webViewContext.getWebView(i).stop() // Stop all loading tabs } localAccountSensitiveSettings.compatibilityMode = checked; for (let i = 0; i < tabs.count; ++i){ - webStackView.getWebView(i).reload() // Reload them with new user agent + webViewContext.getWebView(i).reload() // Reload them with new user agent } } onLaunchBrowserSettings: { Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.browserSettings); } + } Component { @@ -649,11 +516,9 @@ StatusSectionLayout { bookmarksStore: root.bookmarksStore onOpenInNewTab: (url) => root.openUrlInNewTab(url) onEditFavoriteTriggered: { - Global.openPopup(addFavoriteModal, { - modifiyModal: true, - ogUrl: favoriteMenu.currentFavorite ? favoriteMenu.currentFavorite.url : _internal.currentWebView.url, - ogName: favoriteMenu.currentFavorite ? favoriteMenu.currentFavorite.name : _internal.currentWebView.title}) + favoritesContext.openAddFavoritePopup(true, favoriteMenu.currentFavorite) } + } StatusMenu { @@ -680,6 +545,7 @@ StatusSectionLayout { historyMenu.removeItem(object) } } + } Component { @@ -687,23 +553,10 @@ StatusSectionLayout { FavoritesBar { bookmarkModel: root.bookmarksStore.bookmarksModel favoritesMenu: favoriteMenu - onSetAsCurrentWebUrl: (url) => { - if (!_internal.currentWebView) { - console.error("[Browser] currentWebView is null, cannot set URL") - return - } - const newUrl = _internal.determineRealURL(url) - Qt.callLater(function() { - if (_internal.currentWebView) { - _internal.currentWebView.url = newUrl - } - }) - } + onSetAsCurrentWebUrl: (url) => webViewContext.setCurrentWebUrl(url) onOpenInNewTab: (url) => root.openUrlInNewTab(url) onAddFavModalRequested: { - Global.openPopup(addFavoriteModal, {toolbarMode: true, - ogUrl: browserHeader.currentFavorite ? browserHeader.currentFavorite.url : _internal.currentWebView.url, - ogName: browserHeader.currentFavorite ? browserHeader.currentFavorite.name : _internal.currentWebView.title}) + favoritesContext.openAddFavoritePopup(false) } } } @@ -712,7 +565,8 @@ StatusSectionLayout { id: connectorBridge userUID: root.userUID - connectorController: root.connectorController + featureEnabled: root.dappsEnabled + connectorController: root.dappsEnabled ? root.connectorController : null httpUserAgent: { if (localAccountSensitiveSettings.compatibilityMode) { // Google doesn't let you connect if the user agent is Chrome-ish and doesn't satisfy some sort of hidden requirement @@ -739,7 +593,7 @@ StatusSectionLayout { BCBrowserDappsProvider { id: browserDappsProvider - connectorController: root.connectorController + connectorController: root.dappsEnabled ? root.connectorController : null clientId: connectorBridge.clientId clientIdFilter: connectorBridge.clientId } @@ -750,39 +604,13 @@ StatusSectionLayout { downloadsModel: root.downloadsStore.downloadModel downloadsMenu: downloadMenuInst onOpenDownloadClicked: function (downloadComplete, index) { - if (downloadComplete) { - return root.downloadsStore.openFile(index) - } - root.downloadsStore.openDirectory(index) + downloadsContext.openDownloadFromList(downloadComplete, index) } onAddNewDownloadTab: _internal.addNewDownloadTab() onClose: root.showFooter = false } } - Connections { - target: _internal.currentWebView - function onUrlChanged() { - browserHeader.addressBar.text = root.browserRootStore.obtainAddress(_internal.currentWebView.url) - - // Update ConnectorBridge with current dApp metadata - if (_internal.currentWebView && _internal.currentWebView.url) { - connectorBridge.connectorManager.updateDAppUrl( - _internal.currentWebView.url, - _internal.currentWebView.title, - _internal.currentWebView.icon - ) - } - } - } - - Connections { - target: root.bookmarksStore.bookmarksModel - function onModelChanged() { - browserHeader.currentFavorite = Qt.binding(function () {return root.bookmarksStore.getCurrentFavorite(_internal.currentWebView.url)}) - } - } - Connections { target: typeof browserSection !== "undefined" ? browserSection : null function onOpenUrl(url: string) { diff --git a/ui/app/AppLayouts/Browser/adapters/+mobile/ProfileManager.qml b/ui/app/AppLayouts/Browser/adapters/+mobile/ProfileManager.qml new file mode 100644 index 0000000000..b06b9e2276 --- /dev/null +++ b/ui/app/AppLayouts/Browser/adapters/+mobile/ProfileManager.qml @@ -0,0 +1,8 @@ +pragma Singleton +import QtQuick + +QtObject { + function getProfile(profileParams) { + return null + } +} diff --git a/ui/app/AppLayouts/Browser/adapters/MobileWebViewAdapter.qml b/ui/app/AppLayouts/Browser/adapters/+mobile/WebViewAdapter.qml similarity index 83% rename from ui/app/AppLayouts/Browser/adapters/MobileWebViewAdapter.qml rename to ui/app/AppLayouts/Browser/adapters/+mobile/WebViewAdapter.qml index 6c6cb2ba25..304c87b1f7 100644 --- a/ui/app/AppLayouts/Browser/adapters/MobileWebViewAdapter.qml +++ b/ui/app/AppLayouts/Browser/adapters/+mobile/WebViewAdapter.qml @@ -9,6 +9,7 @@ AbstractWebView { required property BrowserStores.BookmarksStore bookmarksStore required property BrowserStores.DownloadsStore downloadsStore + required property var localAccountSensitiveSettings property var findBarComp property var favMenu @@ -52,15 +53,15 @@ AbstractWebView { } function goBack() { - console.warn("MobileWebViewAdapter: goBack not supported yet") + console.warn("WebViewAdapter: goBack not supported yet") } function goForward() { - console.warn("MobileWebViewAdapter: goForward not supported yet") + console.warn("WebViewAdapter: goForward not supported yet") } function goBackOrForward(offset) { - console.warn("MobileWebViewAdapter: goBackOrForward not supported yet") + console.warn("WebViewAdapter: goBackOrForward not supported yet") } function reload() { @@ -70,7 +71,7 @@ AbstractWebView { } function stop() { - console.warn("MobileWebViewAdapter: stop not supported yet") + console.warn("WebViewAdapter: stop not supported yet") } function findText(text, flags) { @@ -82,7 +83,10 @@ AbstractWebView { } function acceptAsNewWindow(request) { - console.warn("MobileWebViewAdapter: acceptAsNewWindow not supported") + console.warn("WebViewAdapter: acceptAsNewWindow not supported") + } + + function detachView() { } function triggerWebAction(action) { @@ -100,7 +104,7 @@ AbstractWebView { reload() break default: - console.warn("MobileWebViewAdapter: Web action not supported:", action) + console.warn("WebViewAdapter: Web action not supported:", action) } } } diff --git a/ui/app/AppLayouts/Browser/adapters/AbstractWebView.qml b/ui/app/AppLayouts/Browser/adapters/AbstractWebView.qml index 05bc67b552..b7887d3586 100644 --- a/ui/app/AppLayouts/Browser/adapters/AbstractWebView.qml +++ b/ui/app/AppLayouts/Browser/adapters/AbstractWebView.qml @@ -52,6 +52,25 @@ Item { RequestClose = 35 } + // === Download States (constants for cross-platform compatibility) === + // These map to WebEngineDownloadRequest.DownloadState enum on desktop + enum DownloadState { + DownloadRequested = 0, + DownloadInProgress = 1, + DownloadCompleted = 2, + DownloadCancelled = 3, + DownloadInterrupted = 4 + } + + // === JavaScript Dialog Types (constants for cross-platform compatibility) === + // These map to JavaScriptDialogRequest.DialogType enum on desktop + enum JavaScriptDialogType { + DialogTypeAlert = 0, + DialogTypeConfirm = 1, + DialogTypePrompt = 2, + DialogTypeUnload = 3 + } + signal linkHovered(string hoveredUrl) signal windowCloseRequested() signal downloadRequested(var download) @@ -76,6 +95,7 @@ Item { function findText(text, flags) {} function changeZoomFactor(factor) {} function acceptAsNewWindow(request) {} + function detachView() {} function triggerWebAction(action) { console.warn("AbstractWebView: triggerWebAction not implemented") } } diff --git a/ui/app/AppLayouts/Browser/provider/qml/ProfileManager.qml b/ui/app/AppLayouts/Browser/adapters/ProfileManager.qml similarity index 90% rename from ui/app/AppLayouts/Browser/provider/qml/ProfileManager.qml rename to ui/app/AppLayouts/Browser/adapters/ProfileManager.qml index c21bb895bc..c03d26dfe2 100644 --- a/ui/app/AppLayouts/Browser/provider/qml/ProfileManager.qml +++ b/ui/app/AppLayouts/Browser/adapters/ProfileManager.qml @@ -20,11 +20,14 @@ QtObject { sourceUrl: path, injectionPoint: WebEngineScript.DocumentCreation, worldId: WebEngineScript.MainWorld, - runsOnSubframes: runOnSubFrames + runsOnSubFrames: runOnSubFrames } } function _getProfilePrototype(storageName, offTheRecord) { + const storageNameProp = offTheRecord + ? "" + : `storageName: "${storageName.replace(/"/g, '\\"')}"` const persistentCookiesPolicy = offTheRecord ? "persistentCookiesPolicy: WebEngineProfile.NoPersistentCookies" : "" @@ -32,7 +35,7 @@ QtObject { return Qt.createQmlObject(` import QtWebEngine WebEngineProfilePrototype { - storageName: "${storageName.replace(/"/g, '\\"')}" + ${storageNameProp} ${persistentCookiesPolicy} } `, root, "ProfilePrototype_" + storageName) diff --git a/ui/app/AppLayouts/Browser/adapters/WebEngineAdapter.qml b/ui/app/AppLayouts/Browser/adapters/WebViewAdapter.qml similarity index 54% rename from ui/app/AppLayouts/Browser/adapters/WebEngineAdapter.qml rename to ui/app/AppLayouts/Browser/adapters/WebViewAdapter.qml index fbb71868d2..a2300cf5a3 100644 --- a/ui/app/AppLayouts/Browser/adapters/WebEngineAdapter.qml +++ b/ui/app/AppLayouts/Browser/adapters/WebViewAdapter.qml @@ -4,16 +4,18 @@ import QtWebEngine import StatusQ.Core.Theme import AppLayouts.Browser.views -import AppLayouts.Browser.provider.qml AbstractWebView { id: root property bool enableJsLogs: false + required property var localAccountSensitiveSettings + property var bookmarksStore + property var downloadsStore property var profile: ProfileManager.getProfile(root.profileParams) - // Expose BrowserWebEngineView properties + // Expose internal WebEngineView properties property alias url: webView.url readonly property alias title: webView.title readonly property alias loading: webView.loading @@ -42,6 +44,16 @@ AbstractWebView { function findText(text, flags) { webView.findText(text, flags) } function changeZoomFactor(factor) { webView.changeZoomFactor(factor) } function acceptAsNewWindow(request) { request.openIn(webView) } + function detachView() { + // Detach internal views from scene graph before destroy. + webView.webChannel = null + devToolsView.inspectedView = null + webView.stop() + webView.visible = false + webView.parent = null + devToolsView.visible = false + devToolsView.parent = null + } function triggerWebAction(action) { // Map AbstractWebView.WebAction to WebEngineView.WebAction switch (action) { @@ -72,26 +84,95 @@ AbstractWebView { case AbstractWebView.WebAction.PasteAndMatchStyle: webView.triggerWebAction(WebEngineView.PasteAndMatchStyle); break default: - console.warn("WebEngineAdapter: Unknown web action:", action) + console.warn("WebViewAdapter: Unknown web action:", action) } } - BrowserWebEngineView { + WebEngineView { id: webView anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: root.devToolsEnabled ? devToolsView.top : parent.bottom + focus: true + + property bool htmlPageLoaded: false + backgroundColor: Theme.palette.background + + settings.autoLoadImages: root.localAccountSensitiveSettings.autoLoadImages + settings.javascriptEnabled: root.localAccountSensitiveSettings.javaScriptEnabled + settings.errorPageEnabled: root.localAccountSensitiveSettings.errorPageEnabled + settings.pluginsEnabled: root.localAccountSensitiveSettings.pluginsEnabled + settings.autoLoadIconsForPage: root.localAccountSensitiveSettings.autoLoadIconsForPage + settings.touchIconsEnabled: root.localAccountSensitiveSettings.touchIconsEnabled + settings.webRTCPublicInterfacesOnly: root.localAccountSensitiveSettings.webRTCPublicInterfacesOnly + settings.pdfViewerEnabled: root.localAccountSensitiveSettings.pdfViewerEnabled + settings.focusOnNavigationEnabled: true + settings.forceDarkMode: Application.styleHints.colorScheme === Qt.ColorScheme.Dark webChannel: root.webChannel profile: root.profile - enableJsLogs: root.enableJsLogs + onQuotaRequested: function(request) { + if (request.requestedSize <= 5 * 1024 * 1024) + request.accept() + else + request.reject() + } + onRegisterProtocolHandlerRequested: function(request) { + console.log("accepting registerProtocolHandler request for " + + request.scheme + " from " + request.origin) + request.accept() + } + onRenderProcessTerminated: function(terminationStatus, exitCode) { + var status = "" + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)" + break + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)" + break + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)" + break + case WebEngineView.KilledTerminationStatus: + status = "(killed)" + break + } + console.warn("Render process exited with code " + exitCode + " " + status) + } + onSelectClientCertificate: function(selection) { + selection.certificates[0].select() + } + onLoadingChanged: function(loadRequest) { + if (loadRequest.status === WebEngineView.LoadStartedStatus) { + webView.htmlPageLoaded = false + } + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { + webView.htmlPageLoaded = true + } + } + onLoadProgressChanged: function(progress) { + if (progress >= 10) + webView.htmlPageLoaded = true + } + onNavigationRequested: function(request) { + if (request.url.toString().startsWith("file:/")) { + console.log("Local file browsing is disabled") + request.reject() + } + } + onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) { + const isOurScript = ScriptUtils.isOurInjectedScript(sourceID, root.profile) + if (isOurScript || root.enableJsLogs) + console.log("[WebEngine]", sourceID + ":" + lineNumber, message) + } onLinkHovered: (hoveredUrl) => root.linkHovered(hoveredUrl) onWindowCloseRequested: root.windowCloseRequested() onNewWindowRequested: (request) => { if (!request.userInitiated) { - console.warn("Warning: Blocked a popup window."); + console.warn("Warning: Blocked a popup window.") } else { const makeCurrent = request.destination !== WebEngineNewWindowRequest.InNewBackgroundTab root.newWindowRequested(makeCurrent, request.requestedUrl, (tab) => tab.acceptAsNewWindow(request)) @@ -100,7 +181,19 @@ AbstractWebView { onCertificateError: (error) => root.certificateError(error) onJavaScriptDialogRequested: (request) => root.javaScriptDialogRequested(request) onFindTextFinished: (result) => root.findTextFinished(result) - onShowFindBar: (numberOfMatches, activeMatch) => root.findTextFinished({numberOfMatches, activeMatch}) + } + + Connections { + target: root.profile + function onDownloadRequested(download) { + // Profile emits for all tabs sharing it; forward only owner view. + if (download?.view && download.view !== webView) + return + // For viewless downloads, only visible adapter forwards to avoid fan-out. + if (!download?.view && !root.visible) + return + root.downloadRequested(download) + } } WebEngineView { @@ -118,18 +211,20 @@ AbstractWebView { } } - Connections { - target: root.profile - function onDownloadRequested(download) { - root.downloadRequested(download) - } - } - Connections { // This connection is needed because changing profileParams.offTheRecord doesn't trigger the root.profile update target: root.profileParams function onOffTheRecordChanged() { root.profile = ProfileManager.getProfile(root.profileParams) } + function onUserAgentChanged() { + root.profile = ProfileManager.getProfile(root.profileParams) + } + function onScriptsChanged() { + root.profile = ProfileManager.getProfile(root.profileParams) + } + function onUserIdChanged() { + root.profile = ProfileManager.getProfile(root.profileParams) + } } } diff --git a/ui/app/AppLayouts/Browser/adapters/qmldir b/ui/app/AppLayouts/Browser/adapters/qmldir index 5b9beeb5ff..ad52f5e32a 100644 --- a/ui/app/AppLayouts/Browser/adapters/qmldir +++ b/ui/app/AppLayouts/Browser/adapters/qmldir @@ -1,4 +1,4 @@ +singleton ProfileManager 1.0 ProfileManager.qml AbstractWebView 1.0 AbstractWebView.qml ProfileParams 1.0 ProfileParams.qml -WebEngineAdapter 1.0 WebEngineAdapter.qml -MobileWebViewAdapter 1.0 MobileWebViewAdapter.qml +WebViewAdapter 1.0 WebViewAdapter.qml diff --git a/ui/app/AppLayouts/Browser/panels/DownloadBar.qml b/ui/app/AppLayouts/Browser/panels/DownloadBar.qml index 4399db2da5..df391e428d 100644 --- a/ui/app/AppLayouts/Browser/panels/DownloadBar.qml +++ b/ui/app/AppLayouts/Browser/panels/DownloadBar.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Layouts -import QtWebEngine import StatusQ.Core import StatusQ.Core.Theme @@ -8,6 +7,7 @@ import StatusQ.Controls import utils +import AppLayouts.Browser.adapters import AppLayouts.Browser.controls Rectangle { @@ -44,8 +44,8 @@ Rectangle { readonly property var downloadItem: downloadsModel.downloads[index] isPaused: downloadItem?.isPaused ?? false - isCanceled: downloadItem?.state === WebEngineDownloadRequest.DownloadCancelled ?? false - downloadComplete: downloadItem?.state === WebEngineDownloadRequest.DownloadCompleted ?? false + isCanceled: downloadItem?.state === AbstractWebView.DownloadState.DownloadCancelled ?? false + downloadComplete: downloadItem?.state === AbstractWebView.DownloadState.DownloadCompleted ?? false primaryText: downloadItem?.downloadFileName ?? "" downloadText: { if (isCanceled) { diff --git a/ui/app/AppLayouts/Browser/panels/DownloadView.qml b/ui/app/AppLayouts/Browser/panels/DownloadView.qml index c36365ef67..b693c9e440 100644 --- a/ui/app/AppLayouts/Browser/panels/DownloadView.qml +++ b/ui/app/AppLayouts/Browser/panels/DownloadView.qml @@ -1,11 +1,11 @@ import QtQuick -import QtWebEngine import StatusQ.Core import StatusQ.Core.Theme import utils +import AppLayouts.Browser.adapters import AppLayouts.Browser.controls Rectangle { @@ -40,7 +40,7 @@ Rectangle { width: parent.width isPaused: downloadItem?.isPaused ?? false - isCanceled: downloadItem?.state === WebEngineDownloadRequest.DownloadCancelled ?? false + isCanceled: downloadItem?.state === AbstractWebView.DownloadState.DownloadCancelled ?? false primaryText: downloadItem?.downloadFileName ?? "" downloadText: { if (isCanceled) { @@ -52,7 +52,7 @@ Rectangle { return "%1/%2".arg(Qt.locale().formattedDataSize(downloadItem?.receivedBytes ?? 0, 2, Locale.DataSizeTraditionalFormat)) //e.g. 14.4/109 MB .arg(Qt.locale().formattedDataSize(downloadItem?.totalBytes ?? 0, 2, Locale.DataSizeTraditionalFormat)) } - downloadComplete: downloadItem?.state === WebEngineDownloadRequest.DownloadCompleted ?? false + downloadComplete: downloadItem?.state === AbstractWebView.DownloadState.DownloadCompleted ?? false onItemClicked: { openDownloadClicked(downloadComplete, index) } diff --git a/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml b/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml index aa8e217969..402bec7bdf 100644 --- a/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml +++ b/ui/app/AppLayouts/Browser/popups/DownloadMenu.qml @@ -1,9 +1,9 @@ import QtQuick import QtQuick.Controls -import QtWebEngine import StatusQ.Popups +import AppLayouts.Browser.adapters import AppLayouts.Browser.stores as BrowserStores StatusMenu { @@ -14,8 +14,8 @@ StatusMenu { property int index: -1 property var download: root.downloadsStore.getDownload(index) - readonly property bool downloadCancelled: download?.state === WebEngineDownloadRequest.DownloadCancelled ?? false - readonly property bool downloadComplete: download?.state === WebEngineDownloadRequest.DownloadCompleted ?? false + readonly property bool downloadCancelled: download?.state === AbstractWebView.DownloadState.DownloadCancelled ?? false + readonly property bool downloadComplete: download?.state === AbstractWebView.DownloadState.DownloadCompleted ?? false signal cancelClicked() diff --git a/ui/app/AppLayouts/Browser/popups/JSDialogWindow.qml b/ui/app/AppLayouts/Browser/popups/JSDialogWindow.qml index 1eb942cef2..773a9bc984 100644 --- a/ui/app/AppLayouts/Browser/popups/JSDialogWindow.qml +++ b/ui/app/AppLayouts/Browser/popups/JSDialogWindow.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtWebEngine import StatusQ.Core import StatusQ.Core.Theme @@ -12,6 +11,8 @@ import shared.popups import utils +import AppLayouts.Browser.adapters + // TODO: replace with StatusDialog ModalPopup { id: root @@ -28,10 +29,10 @@ ModalPopup { Component.onCompleted: { root.title = request.securityOrigin; message.text = request.message; - if(request.type === JavaScriptDialogRequest.DialogTypeAlert) { + if(request.type === AbstractWebView.JavaScriptDialogType.DialogTypeAlert) { cancelButton.visible = false; } - if(request.type === JavaScriptDialogRequest.DialogTypePrompt) { + if(request.type === AbstractWebView.JavaScriptDialogType.DialogTypePrompt) { prompt.text = request.defaultText; prompt.visible = true; svMessage.height = 75; diff --git a/ui/app/AppLayouts/Browser/provider/js/ethereum_wrapper.js b/ui/app/AppLayouts/Browser/provider/js/ethereum_wrapper.js index 451399c077..9c244b6a6b 100644 --- a/ui/app/AppLayouts/Browser/provider/js/ethereum_wrapper.js +++ b/ui/app/AppLayouts/Browser/provider/js/ethereum_wrapper.js @@ -18,7 +18,9 @@ const EthereumWrapper = (function() { this.listeners = new Map(); // event -> Set this.nativeEthereum = nativeEthereum; - this.requestIdCounter = 1; // async requests + // Start from a per-instance random base to avoid requestId collisions + // between different frames/tabs that share connector callbacks. + this.requestIdCounter = Math.floor(Math.random() * 1000000000) + 1; this.pendingRequests = new Map(); // requestId -> { resolve, reject, timestamp } this.requestTimeout = 600000; // 10min timeout for pending requests. (nim side has it's own timeouts) this.timeoutCheckInterval = 10000; diff --git a/ui/app/AppLayouts/Browser/provider/qml/ConnectorBridge.qml b/ui/app/AppLayouts/Browser/provider/qml/ConnectorBridge.qml index 89b411de81..3ec22a929a 100644 --- a/ui/app/AppLayouts/Browser/provider/qml/ConnectorBridge.qml +++ b/ui/app/AppLayouts/Browser/provider/qml/ConnectorBridge.qml @@ -17,6 +17,7 @@ QtObject { id: root required property string userUID + property bool featureEnabled: true required property var connectorController property string httpUserAgent: "" // Custom user agent for web profiles @@ -34,12 +35,12 @@ QtObject { offTheRecord: true } - readonly property var scriptPaths: [ + readonly property var scriptPaths: root.featureEnabled ? [ { path: Qt.resolvedUrl("../js/qwebchannel.js"), runOnSubFrames: true }, { path: Qt.resolvedUrl("../js/ethereum_wrapper.js"), runOnSubFrames: true }, { path: Qt.resolvedUrl("../js/eip6963_announcer.js"), runOnSubFrames: false }, { path: Qt.resolvedUrl("../js/ethereum_injector.js"), runOnSubFrames: true } - ] + ] : [] readonly property alias dappUrl: connectorManager.dappUrl readonly property alias dappOrigin: connectorManager.dappOrigin diff --git a/ui/app/AppLayouts/Browser/provider/qml/ConnectorManager.qml b/ui/app/AppLayouts/Browser/provider/qml/ConnectorManager.qml index 77ed2b8fcd..e3addc9b57 100644 --- a/ui/app/AppLayouts/Browser/provider/qml/ConnectorManager.qml +++ b/ui/app/AppLayouts/Browser/provider/qml/ConnectorManager.qml @@ -133,12 +133,12 @@ QtObject { } } - function changeAccount(newAccount) { - if (connectorController) { - connectorController.disconnect(dappOrigin, clientId) - connectorController.changeAccount(dappOrigin, clientId, newAccount) - } - } + function changeAccount(newAccount) { + if (connectorController) { + connectorController.disconnect(dappOrigin, clientId) + connectorController.changeAccount(dappOrigin, clientId, newAccount) + } + } function updateDAppUrl(url, name, iconUrl) { if (!url) return diff --git a/ui/app/AppLayouts/Browser/provider/qml/qmldir b/ui/app/AppLayouts/Browser/provider/qml/qmldir index d0859bc40a..f3fff6bc37 100644 --- a/ui/app/AppLayouts/Browser/provider/qml/qmldir +++ b/ui/app/AppLayouts/Browser/provider/qml/qmldir @@ -1,4 +1,3 @@ -singleton ProfileManager 1.0 ProfileManager.qml ConnectorBridge 1.0 ConnectorBridge.qml ConnectorManager 1.0 ConnectorManager.qml Eip1193ProviderAdapter 1.0 Eip1193ProviderAdapter.qml diff --git a/ui/app/AppLayouts/Browser/views/BrowserShortcutActions.qml b/ui/app/AppLayouts/Browser/views/BrowserShortcutActions.qml index c1f62475c3..85233f7e19 100644 --- a/ui/app/AppLayouts/Browser/views/BrowserShortcutActions.qml +++ b/ui/app/AppLayouts/Browser/views/BrowserShortcutActions.qml @@ -58,6 +58,11 @@ QObject { property var currentWebView property var findBarComponent property var browserHeaderComponent + function triggerWebAction(action) { + if (!currentWebView) + return + currentWebView.triggerWebAction(action) + } Shortcut { sequences: ["Ctrl+L", "F6"] @@ -68,16 +73,13 @@ QObject { Shortcut { sequences: [StandardKey.Refresh] onActivated: { - if (currentWebView) - currentWebView.triggerWebAction(AbstractWebView.WebAction.Reload) + triggerWebAction(AbstractWebView.WebAction.Reload) } } Shortcut { sequences: [StandardKey.Close] onActivated: { - if (currentWebView) { - currentWebView.triggerWebAction(AbstractWebView.WebAction.RequestClose) - } + triggerWebAction(AbstractWebView.WebAction.RequestClose) } } Shortcut { @@ -85,45 +87,44 @@ QObject { onActivated: { if (findBarComponent.visible) findBarComponent.visible = false; - if (currentWebView) - currentWebView.triggerWebAction(AbstractWebView.WebAction.Stop) + triggerWebAction(AbstractWebView.WebAction.Stop) } } Shortcut { sequences: [StandardKey.Copy] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Copy) + onActivated: triggerWebAction(AbstractWebView.WebAction.Copy) } Shortcut { sequences: [StandardKey.Cut] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Cut) + onActivated: triggerWebAction(AbstractWebView.WebAction.Cut) } Shortcut { sequences: [StandardKey.Paste] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Paste) + onActivated: triggerWebAction(AbstractWebView.WebAction.Paste) } Shortcut { sequence: "Shift+"+StandardKey.Paste - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.PasteAndMatchStyle) + onActivated: triggerWebAction(AbstractWebView.WebAction.PasteAndMatchStyle) } Shortcut { sequences: [StandardKey.SelectAll] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.SelectAll) + onActivated: triggerWebAction(AbstractWebView.WebAction.SelectAll) } Shortcut { sequences: [StandardKey.Undo] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Undo) + onActivated: triggerWebAction(AbstractWebView.WebAction.Undo) } Shortcut { sequences: [StandardKey.Redo] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Redo) + onActivated: triggerWebAction(AbstractWebView.WebAction.Redo) } Shortcut { sequences: [StandardKey.Back] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Back) + onActivated: triggerWebAction(AbstractWebView.WebAction.Back) } Shortcut { sequences: [StandardKey.Forward] - onActivated: currentWebView.triggerWebAction(AbstractWebView.WebAction.Forward) + onActivated: triggerWebAction(AbstractWebView.WebAction.Forward) } Shortcut { sequences: [StandardKey.FindNext] diff --git a/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml b/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml deleted file mode 100644 index 9d8a161f49..0000000000 --- a/ui/app/AppLayouts/Browser/views/BrowserWebEngineView.qml +++ /dev/null @@ -1,108 +0,0 @@ -import QtQuick -import QtWebEngine - -import StatusQ.Core.Theme - -import "ScriptUtils.js" as ScriptUtils - -WebEngineView { - id: root - - property bool enableJsLogs: false - property bool htmlPageLoaded: false - - signal showFindBar(int numberOfMatches, int activeMatch) - signal resetFindBar() - - focus: true - - function changeZoomFactor(newFactor) { - zoomFactor = newFactor - } - - backgroundColor: Theme.palette.background - - settings.autoLoadImages: localAccountSensitiveSettings.autoLoadImages - settings.javascriptEnabled: localAccountSensitiveSettings.javaScriptEnabled - settings.errorPageEnabled: localAccountSensitiveSettings.errorPageEnabled - settings.pluginsEnabled: localAccountSensitiveSettings.pluginsEnabled - settings.autoLoadIconsForPage: localAccountSensitiveSettings.autoLoadIconsForPage - settings.touchIconsEnabled: localAccountSensitiveSettings.touchIconsEnabled - settings.webRTCPublicInterfacesOnly: localAccountSensitiveSettings.webRTCPublicInterfacesOnly - settings.pdfViewerEnabled: localAccountSensitiveSettings.pdfViewerEnabled - settings.focusOnNavigationEnabled: true - settings.forceDarkMode: Application.styleHints.colorScheme === Qt.ColorScheme.Dark - - onQuotaRequested: function(request) { - if (request.requestedSize <= 5 * 1024 * 1024) - request.accept(); - else - request.reject(); - } - - onRegisterProtocolHandlerRequested: function(request) { - console.log("accepting registerProtocolHandler request for " - + request.scheme + " from " + request.origin); - request.accept(); - } - - onRenderProcessTerminated: function(terminationStatus, exitCode) { - var status = ""; - switch (terminationStatus) { - case WebEngineView.NormalTerminationStatus: - status = "(normal exit)"; - break; - case WebEngineView.AbnormalTerminationStatus: - status = "(abnormal exit)"; - break; - case WebEngineView.CrashedTerminationStatus: - status = "(crashed)"; - break; - case WebEngineView.KilledTerminationStatus: - status = "(killed)"; - break; - } - - console.warn("Render process exited with code " + exitCode + " " + status); - } - - onSelectClientCertificate: function(selection) { - selection.certificates[0].select(); - } - - onFindTextFinished: function(result) { - root.showFindBar(result.numberOfMatches, result.activeMatch) - } - - onLoadingChanged: function(loadRequest) { - if (loadRequest.status === WebEngineView.LoadStartedStatus) { - root.htmlPageLoaded = false - root.resetFindBar() - } - if (loadRequest.status === WebEngineView.LoadSucceededStatus) { - root.htmlPageLoaded = true - } - } - - onLoadProgressChanged: function(progress) { - if (progress >= 10) { - // Some real content rendered - htmlPageLoaded = true - } - } - - onNavigationRequested: function (request) { - if(request.url.toString().startsWith("file:/")){ - console.log("Local file browsing is disabled" ) - request.reject() - } - } - - onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) { - // Check if the message is from our injected scripts - const isOurScript = ScriptUtils.isOurInjectedScript(sourceID, root.profile); - if (isOurScript || root.enableJsLogs) { - console.log("[WebEngine]", sourceID + ":" + lineNumber, message); - } - } -} diff --git a/ui/app/AppLayouts/Browser/views/qmldir b/ui/app/AppLayouts/Browser/views/qmldir index 404ff84c20..e991d46145 100644 --- a/ui/app/AppLayouts/Browser/views/qmldir +++ b/ui/app/AppLayouts/Browser/views/qmldir @@ -1,4 +1,4 @@ -BrowserWebEngineView 1.0 BrowserWebEngineView.qml WebViewContainer 1.0 WebViewContainer.qml BrowserShortcutActions 1.0 BrowserShortcutActions.qml EmptyWebPage 1.0 EmptyWebPage.qml +ScriptUtils 1.0 ScriptUtils.js diff --git a/ui/app/AppLayouts/Browser/webview/BrowserDialogsContext.qml b/ui/app/AppLayouts/Browser/webview/BrowserDialogsContext.qml new file mode 100644 index 0000000000..13cec1a0de --- /dev/null +++ b/ui/app/AppLayouts/Browser/webview/BrowserDialogsContext.qml @@ -0,0 +1,43 @@ +import QtQuick +import StatusQ.Core.Utils as SQUtils + +QtObject { + id: root + + required property var networksStore + required property var browserActivityStore + required property var browserWalletStore + required property var openPopupFn + required property Component jsDialogComponent + required property Item dialogParent + + function openHistoryMenu(historyMenu) { + historyMenu.open() + } + + function openSettingsMenu(settingsMenu) { + settingsMenu.open() + } + + function openWalletMenu(browserWalletMenu) { + // Initialize activity filters before opening popup. + const activeChainIds = SQUtils.ModelUtils.modelToFlatArray( + networksStore.activeNetworks, "chainId") + if (activeChainIds.length > 0) { + browserActivityStore.activityController.setFilterChainsJson( + JSON.stringify(activeChainIds), true) + } + + const currentAddress = browserWalletStore.dappBrowserAccount.address + browserActivityStore.activityController.setFilterAddressesJson( + JSON.stringify([currentAddress])) + openPopupFn(browserWalletMenu) + } + + function openJsDialog(request) { + request.accepted = true + var dialog = jsDialogComponent.createObject(dialogParent, {"request": request}) + if (dialog) + dialog.open() + } +} diff --git a/ui/app/AppLayouts/Browser/webview/BrowserDownloadsContext.qml b/ui/app/AppLayouts/Browser/webview/BrowserDownloadsContext.qml new file mode 100644 index 0000000000..798b67deb8 --- /dev/null +++ b/ui/app/AppLayouts/Browser/webview/BrowserDownloadsContext.qml @@ -0,0 +1,39 @@ +import QtQuick + +QtObject { + id: root + + required property var downloadsStore + required property var tabsModel + required property var getWebViewFn + required property var removeViewFn + required property var setFooterVisibleFn + + function handleDownloadRequest(download) { + if (!download) + return + + downloadsStore.addDownload(download) + download.accept() + setFooterVisibleFn(true) + + // Close tabs that were opened only to trigger a download. + if (!download.view) + return + + for (var i = 0; i < tabsModel.count; ++i) { + var tab = getWebViewFn(i) + if (tab === download.view && !tab.htmlPageLoaded && tab.title === "") { + removeViewFn(i) + break + } + } + } + + function openDownloadFromList(downloadComplete, index) { + if (downloadComplete) + return downloadsStore.openFile(index) + + downloadsStore.openDirectory(index) + } +} diff --git a/ui/app/AppLayouts/Browser/webview/BrowserFavoritesContext.qml b/ui/app/AppLayouts/Browser/webview/BrowserFavoritesContext.qml new file mode 100644 index 0000000000..866e29fbaf --- /dev/null +++ b/ui/app/AppLayouts/Browser/webview/BrowserFavoritesContext.qml @@ -0,0 +1,47 @@ +import QtQuick + +import QtModelsToolkit + +QtObject { + id: root + + required property var currentWebView + required property var bookmarksStore + required property bool shouldShowFavoritesBar + required property var openPopupFn + required property var addFavoriteModal + + readonly property bool favoritesBarActive: shouldShowFavoritesBar && + bookmarksStore.bookmarksModel.ModelCount.count > 0 + readonly property var currentUrl: (currentWebView && currentWebView.url) + ? currentWebView.url + : "" + readonly property string currentTitle: currentWebView ? currentWebView.title : "" + + readonly property var currentViewBookmarkEntry: ModelEntry { + sourceModel: root.bookmarksStore.bookmarksModel + key: "url" + value: root.currentUrl ? root.currentUrl.toString() : "" + } + + readonly property bool currentTabIsBookmark: currentViewBookmarkEntry.available && + !!currentViewBookmarkEntry.item + + function buildAddFavoritePopupParams(modifyModal = false, fallbackItem = null) { + var bookmarkItem = currentViewBookmarkEntry.available ? currentViewBookmarkEntry.item : null + var sourceItem = fallbackItem || bookmarkItem + var sourceUrl = sourceItem ? sourceItem.url : currentUrl + var sourceName = sourceItem ? sourceItem.name : currentTitle + return { + modifiyModal: modifyModal, + toolbarMode: true, + ogUrl: sourceUrl, + ogName: sourceName + } + } + + function openAddFavoritePopup(modifyModal = false, fallbackItem = null) { + openPopupFn(addFavoriteModal, buildAddFavoritePopupParams(modifyModal, fallbackItem)) + } + +} diff --git a/ui/app/AppLayouts/Browser/webview/BrowserWebViewContext.qml b/ui/app/AppLayouts/Browser/webview/BrowserWebViewContext.qml new file mode 100644 index 0000000000..cc4fc39082 --- /dev/null +++ b/ui/app/AppLayouts/Browser/webview/BrowserWebViewContext.qml @@ -0,0 +1,200 @@ +import QtQuick +import QtQuick.Controls + +import utils +import StatusQ.Core.Utils as SQUtils + +import AppLayouts.Browser.adapters + +Item { + id: root + visible: false + + required property bool thirdpartyServicesEnabled + required property bool isDebugEnabled + required property bool isMobile + + required property var browserSettings + required property var webChannel + + required property Item hostStackLayout + required property var tabsModel + required property ProfileParams defaultProfileParams + + required property var bookmarksStore + required property var downloadsStore + + required property var determineRealURLFn + required property var downloadRequestHandler + required property var sslErrorHandler + required property var jsDialogHandler + required property var findTextFinishedHandler + + enum ContentMode { + WebContent = 0, + DownloadContent, + EmptyContent + } + + readonly property Item currentWebView: tabsModel.currentIndex < tabsModel.count ? getCurrentWebView() : null + readonly property int currentContentMode: { + if (!currentWebView) + return BrowserWebViewContext.ContentMode.EmptyContent + if (currentWebView.isDownloadView) + return BrowserWebViewContext.ContentMode.DownloadContent + if (!currentWebView.url?.toString()) + return BrowserWebViewContext.ContentMode.EmptyContent + return BrowserWebViewContext.ContentMode.WebContent + } + + function createEmptyTab(profileParams, createAsStartPage = false, focusOnNewTab = true, url = undefined) { + focusOnNewTab = focusOnNewTab && !createAsStartPage + + var webview = webViewAdapterComponent.createObject(hostStackLayout, { + profileParams: profileParams, + isDownloadView: false + }) + + tabsModel.createEmptyTab(createAsStartPage, focusOnNewTab, webview) + + if (createAsStartPage && thirdpartyServicesEnabled) { + webview.url = Constants.browserDefaultHomepage + } else if (url !== undefined) { + webview.url = url + } else if (!!browserSettings.browserHomepage) { + webview.url = determineRealURLFn(browserSettings.browserHomepage) + } + + return webview + } + + function createDownloadTab(profileParams) { + var webview = webViewAdapterComponent.createObject(hostStackLayout, { + profileParams: profileParams, + isDownloadView: true + }) + tabsModel.createDownloadTab() + return webview + } + + function getCurrentWebView() { // -> WebEngineView/WebView + return getWebView(tabsModel.currentIndex) + } + + function getWebView(index) { // -> WebEngineView/WebView + return hostStackLayout.children[index] + } + + function setCurrentWebUrl(url) { + if (!currentWebView) { + console.error("[Browser] currentWebView is null, cannot set URL") + return + } + + const newUrl = determineRealURLFn(url) + Qt.callLater(function() { + if (currentWebView) + currentWebView.url = newUrl + }) + } + + function goBackCurrent() { + if (!currentWebView) + return + currentWebView.goBack() + } + + function goForwardCurrent() { + if (!currentWebView) + return + currentWebView.goForward() + } + + function reloadCurrent() { + if (!currentWebView) + return + currentWebView.reload() + } + + function stopCurrent() { + if (!currentWebView) + return + currentWebView.stop() + } + + function findTextCurrent(text, backward = false) { + if (!currentWebView || !text) + return + + if (backward) { + currentWebView.findText(text, currentWebView.findBackward) + return + } + + currentWebView.findText(text) + } + + function setIncognitoCurrent(checked) { + if (!currentWebView) + return + currentWebView.profileParams.offTheRecord = checked + } + + function changeZoomCurrent(delta) { + if (!currentWebView) + return + currentWebView.changeZoomFactor(currentWebView.zoomFactor + delta) + } + + function resetZoomCurrent() { + if (!currentWebView) + return + currentWebView.changeZoomFactor(1.0) + } + + function removeView(index) { + if (index < 0 || index >= tabsModel.count) + return + + var view = getWebView(index) + if (tabsModel.count <= 1) { + var fallbackProfileParams = currentWebView ? currentWebView.profileParams : defaultProfileParams + createEmptyTab(fallbackProfileParams, true) + } + tabsModel.removeTab(index) + if (!view) + return + view.visible = false + view.enabled = false + view.focus = false + view.detachView() + view.parent = null + view.destroy() + } + + Component { + id: webViewAdapterComponent + WebViewAdapter { + visible: !SQUtils.Utils.hasPopups(Overlay.overlay.children) || !root.isMobile + enabled: visible + + bookmarksStore: root.bookmarksStore + downloadsStore: root.downloadsStore + webChannel: root.webChannel + enableJsLogs: root.isDebugEnabled + localAccountSensitiveSettings: root.browserSettings + devToolsEnabled: root.browserSettings.devToolsEnabled + + onWindowCloseRequested: root.removeView(StackLayout.index) + onNewWindowRequested: (makeCurrent, requestedUrl, callback) => { + var profileParams = root.currentWebView ? root.currentWebView.profileParams : root.defaultProfileParams + var tab = root.createEmptyTab(profileParams, false, makeCurrent, requestedUrl) + callback(tab) + } + onDownloadRequested: (download) => root.downloadRequestHandler(download) + onCertificateError: (error) => root.sslErrorHandler(error) + onJavaScriptDialogRequested: (request) => root.jsDialogHandler(request) + onFindTextFinished: (result) => root.findTextFinishedHandler(result) + } + } +} diff --git a/ui/app/AppLayouts/Browser/webview/qmldir b/ui/app/AppLayouts/Browser/webview/qmldir new file mode 100644 index 0000000000..8dee24f94e --- /dev/null +++ b/ui/app/AppLayouts/Browser/webview/qmldir @@ -0,0 +1,5 @@ +BrowserWebViewContext 1.0 BrowserWebViewContext.qml +BrowserOverlayContext 1.0 BrowserOverlayContext.qml +BrowserFavoritesContext 1.0 BrowserFavoritesContext.qml +BrowserDialogsContext 1.0 BrowserDialogsContext.qml +BrowserDownloadsContext 1.0 BrowserDownloadsContext.qml diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 99ed1fd2c9..8d49085146 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -1504,7 +1504,12 @@ Item { Item { anchors.fill: parent - anchors.leftMargin: sidebar.alwaysVisible ? sidebar.width : undefined + readonly property bool offsetBySidebar: sidebar.alwaysVisible + || d.activeSectionType === Constants.appSection.browser + readonly property real sidebarOffset: offsetBySidebar + ? sidebar.width * (sidebar.alwaysVisible ? 1.0 : sidebar.position) + : 0 + anchors.leftMargin: sidebarOffset StackLayout { id: appView @@ -1851,9 +1856,7 @@ Item { restoreMode: Binding.RestoreNone } - sourceComponent: (appMain.rootStore.thirdpartyServicesEnabled && !SQUtils.Utils.isMobile) - ? browserLayout - : browserPrivacyWall + sourceComponent: appMain.rootStore.thirdpartyServicesEnabled ? browserLayout: browserPrivacyWall Component { id: browserPrivacyWall @@ -1873,6 +1876,7 @@ Item { isMobile: SQUtils.Utils.isMobile userUID: appMain.profileStore.pubKey thirdpartyServicesEnabled: appMain.rootStore.thirdpartyServicesEnabled + dappsEnabled: featureFlagsStore.dappsEnabled bookmarksStore: BrowserStores.BookmarksStore {} downloadsStore: BrowserStores.DownloadsStore {} browserRootStore: BrowserStores.BrowserRootStore {} @@ -2932,7 +2936,7 @@ Item { accountsModel: WalletStores.RootStore.nonWatchAccounts } bcSdk: DappsConnectorSDK { - enabled: featureFlagsStore.connectorEnabled && WalletStores.RootStore.walletSectionInst.walletReady + enabled: featureFlagsStore.dappsEnabled && WalletStores.RootStore.walletSectionInst.walletReady excludeClientIds: ["walletconnect"] store: SharedStores.BrowserConnectStore { controller: WalletStores.RootStore.dappsConnectorController diff --git a/ui/app/mainui/panels/PrimaryNavSidebar.qml b/ui/app/mainui/panels/PrimaryNavSidebar.qml index 478c465cde..e761b53466 100644 --- a/ui/app/mainui/panels/PrimaryNavSidebar.qml +++ b/ui/app/mainui/panels/PrimaryNavSidebar.qml @@ -109,11 +109,7 @@ Control { : root.Theme.palette.privacyColors.primary readonly property int containerBgRadius: root.Theme.defaultPadding - readonly property bool hasPopups: root.Overlay.overlay.children.filter( - item => { - const str = item.toString() - return str.includes("QQuickPopupItem") && !str.includes("StatusToolTip") - }).length + readonly property bool hasPopups: SQUtils.Utils.hasPopups(root.Overlay.overlay.children) onHasPopupsChanged: { if (d.hasPopups) { diff --git a/ui/imports/shared/popups/walletconnect/panels/IntentionPanel.qml b/ui/imports/shared/popups/walletconnect/panels/IntentionPanel.qml deleted file mode 100644 index 4891356695..0000000000 --- a/ui/imports/shared/popups/walletconnect/panels/IntentionPanel.qml +++ /dev/null @@ -1,112 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -import StatusQ.Components -import StatusQ.Core -import StatusQ.Core.Theme - -import utils - -ColumnLayout { - id: root - - spacing: 8 - - required property string dappName - required property url dappIcon - required property var account - - property string userDisplayNaming - - // Icons - Item { - Layout.fillWidth: true - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 8 - - StatusRoundedImage { - width: height - height: parent.height - - anchors.horizontalCenter: parent.horizontalCenter - anchors.horizontalCenterOffset: -16 - anchors.verticalCenter: parent.verticalCenter - - image.source: root.dappIcon - } - StatusRoundIcon { - anchors.horizontalCenter: parent.horizontalCenter - anchors.horizontalCenterOffset: 16 - anchors.verticalCenter: parent.verticalCenter - - asset: StatusAssetSettings { - width: 24 - height: 24 - color: root.Theme.palette.primaryColor1 - bgWidth: 40 - bgHeight: 40 - bgColor: root.Theme.palette.desktopBlue10 - bgRadius: bgWidth / 2 - bgBorderWidth: 2 - bgBorderColor: root.Theme.palette.statusAppLayout.backgroundColor - source: Assets.svg("sign") - } - } - } - - // Names and intentions - StatusBaseText { - text: qsTr("%1 wants you to %2 with %3").arg(dappName).arg(root.userDisplayNaming).arg(account.name) - - Layout.preferredWidth: 400 - Layout.alignment: Qt.AlignHCenter - - font.pixelSize: Theme.primaryTextFontSize - font.weight: Font.DemiBold - - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - - // TODO #14762: externalize as a InfoPill and merge base implementation with - // the existing IssuePill reusable component - Rectangle { - Layout.preferredWidth: operationStatusLayout.implicitWidth + 24 - Layout.preferredHeight: operationStatusLayout.implicitHeight + 14 - - Layout.alignment: Qt.AlignHCenter - - visible: true - - border.color: Theme.palette.successColor2 - border.width: 1 - color: "transparent" - radius: height / 2 - - RowLayout { - id: operationStatusLayout - - spacing: 8 - - anchors.centerIn: parent - - StatusIcon { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - - visible: true - - color: Theme.palette.directColor1 - icon: "info" - } - - StatusBaseText { - text: qsTr("Only sign if you trust the dApp") - - font.pixelSize: Theme.tertiaryTextFontSize - color: Theme.palette.directColor1 - } - } - } -} diff --git a/ui/imports/shared/popups/walletconnect/panels/MaxFeesDisplay.qml b/ui/imports/shared/popups/walletconnect/panels/MaxFeesDisplay.qml deleted file mode 100644 index 39b58ed581..0000000000 --- a/ui/imports/shared/popups/walletconnect/panels/MaxFeesDisplay.qml +++ /dev/null @@ -1,36 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -import StatusQ.Core -import StatusQ.Core.Theme - -ColumnLayout { - id: root - - property alias maxFeesText: maxFeesDisplay.text - property alias feesTextColor: maxFeesDisplay.color - - StatusBaseText { - text: qsTr("Max fees:") - - font.pixelSize: Theme.tertiaryTextFontSize - color: Theme.palette.directColor1 - } - StatusBaseText { - id: maxFeesDisplay - text: root.maxFeesText - - visible: !!text - - font.pixelSize: Theme.fontSize(16) - - } - StatusBaseText { - text: qsTr("No fees") - - visible: !maxFeesDisplay.visible - - font.pixelSize: maxFeesDisplay.font.pixelSize - font.weight: maxFeesDisplay.font.weight - } -} diff --git a/ui/imports/shared/popups/walletconnect/panels/qmldir b/ui/imports/shared/popups/walletconnect/panels/qmldir index 16db2d9470..a725f4b940 100644 --- a/ui/imports/shared/popups/walletconnect/panels/qmldir +++ b/ui/imports/shared/popups/walletconnect/panels/qmldir @@ -1,3 +1 @@ -MaxFeesDisplay 1.0 MaxFeesDisplay.qml -IntentionPanel 1.0 IntentionPanel.qml -ContentPanel 1.0 ContentPanel.qml +ContentPanel 1.0 ContentPanel.qml