feat(browser): mobile webview (#20178)

* fix(browser): fix pr comments

* fix(browser): fix offset between navigation and webview

* fix(browser): split logic into contexts

* dialogs
* favorites
* webview

* fix(browser): crash when webview tries to render and destroy at the same time

* fix(browser): opened dialogs counter

* fix(browser):  load platform-specific webview

* fix(wc): unused wc files

* fix(browser): separate dapps and websocket logic

* fix(browser): fix pr comments

* fix(browser): fix popup tracker

* fix(browser): fix pr

* fix(browser): fix PR

* fix(browser): fix PR
This commit is contained in:
Andrey Bocharnikov
2026-03-17 22:32:58 +04:00
committed by GitHub
parent b8bf85048f
commit 8950aaf02a
40 changed files with 710 additions and 843 deletions
+12
View File
@@ -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"}
+6
View File
@@ -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}")
+14
View File
@@ -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()
+1
View File
@@ -33,6 +33,7 @@ SplitView {
userUID: "0xdeadbeef"
transactionStore: TransactionStoreMock {}
thirdpartyServicesEnabled: true
dappsEnabled: true
connectorController: null
platformOS: ctrlPlatformOS.currentValue
-4
View File
@@ -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);
+5 -1
View File
@@ -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
@@ -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
}
}
-1
View File
@@ -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
@@ -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
}
}
-1
View File
@@ -67,7 +67,6 @@
<file>StatusQ/Components/StatusToolBar.qml</file>
<file>StatusQ/Components/StatusUserImage.qml</file>
<file>StatusQ/Components/StatusVideo.qml</file>
<file>StatusQ/Components/WebEngineLoader.qml</file>
<file>StatusQ/Components/private/LoadingDotItem.qml</file>
<file>StatusQ/Components/private/StatusComboboxBackground.qml</file>
<file>StatusQ/Components/private/StatusComboboxIndicator.qml</file>
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
<script src="../../../src/StatusQ/Components/private/qwebchannel/qwebchannel.js" defer></script>
<script src="../../../src/StatusQ/Components/private/qwebchannel/helpers.js" defer></script>
</head>
<body>
<h1>Test Page</h1>
</body>
</html>
@@ -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")
}
}
+117 -289
View File
@@ -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) {
@@ -0,0 +1,8 @@
pragma Singleton
import QtQuick
QtObject {
function getProfile(profileParams) {
return null
}
}
@@ -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)
}
}
}
@@ -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") }
}
@@ -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)
@@ -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)
}
}
}
+2 -2
View File
@@ -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
@@ -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) {
@@ -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)
}
@@ -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()
@@ -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;
@@ -18,7 +18,9 @@ const EthereumWrapper = (function() {
this.listeners = new Map(); // event -> Set<handler>
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;
@@ -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
@@ -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
@@ -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
@@ -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]
@@ -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);
}
}
}
+1 -1
View File
@@ -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
@@ -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()
}
}
@@ -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)
}
}
@@ -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))
}
}
@@ -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)
}
}
}
+5
View File
@@ -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
+9 -5
View File
@@ -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
+1 -5
View File
@@ -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) {
@@ -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
}
}
}
}
@@ -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
}
}
@@ -1,3 +1 @@
MaxFeesDisplay 1.0 MaxFeesDisplay.qml
IntentionPanel 1.0 IntentionPanel.qml
ContentPanel 1.0 ContentPanel.qml
ContentPanel 1.0 ContentPanel.qml