This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
powersync-app/nymea-app/ui/RootItem.qml
2025-05-06 15:44:51 +02:00

507 lines
26 KiB
QML

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3
import Qt.labs.settings 1.0
import Nymea 1.0
import NymeaApp.Utils 1.0
import "components"
import "connection"
Item {
id: root
function handleAndroidBackButton() {
return swipeView.currentItem.handleAndroidBackButton()
}
QtObject {
id: d
// We want only one of them pushed at a time
property var currentSettingsPage: null
function pushSettingsPage(page) {
if (d.currentSettingsPage != null) {
d.currentSettingsPage = swipeView.currentItem.pageStack.replace(d.currentSettingsPage, page);
} else {
d.currentSettingsPage = swipeView.currentItem.pageStack.push(page)
}
}
}
function openThingSettings() {
d.pushSettingsPage("thingconfiguration/EditThingsPage.qml")
}
function openMagicSettings() {
d.pushSettingsPage("MagicPage.qml")
}
function openAppSettings() {
d.pushSettingsPage("appsettings/AppSettingsPage.qml")
}
function openSystemSettings() {
d.pushSettingsPage("SettingsPage.qml")
}
function openCustomPage(page) {
d.pushSettingsPage(page)
}
function configureMainView() {
swipeView.currentItem.pageStack.pop(null)
swipeView.currentItem.pageStack.currentItem.configureViews()
}
ColumnLayout {
anchors.fill: parent
spacing: 0
SwipeView {
id: swipeView
Layout.fillHeight: true
Layout.fillWidth: true
interactive: false
currentIndex: configuredHostsModel.currentIndex
onCurrentIndexChanged: visibleHackTimer.start();
// Hack: we only want the current one visible for performance reasons, however, during transitions this might look odd
// as we need to show two views partially at the same time.
// Couldn't find a way to determine the swipeView position, so let's assume it's moving for a second after currentIndex changed
Timer {
id: visibleHackTimer
repeat: false
interval: 500
running: false
}
Repeater {
id: mainRepeater
model: configuredHostsModel
delegate: Item {
height: swipeView.height
width: swipeView.width
clip: true
visible: SwipeView.isCurrentItem || visibleHackTimer.running
readonly property ConfiguredHost configuredHost: configuredHostsModel.get(index)
property var tabSettings: Settings {
category: "tabSettings" + index
property int currentMainViewIndex: -1
}
readonly property Engine engine: configuredHost.engine
readonly property Engine _engine: configuredHost.engine // In case a child cannot use "engine"
Binding {
target: nymeaDiscovery
property: "discovering"
value: engine.jsonRpcClient.currentHost === null
&& (PlatformPermissions.localNetworkPermission === PlatformPermissions.PermissionStatusGranted
// This OR wouldn't be needed but we introduced the permission handling later and the localNetworkPerm can't be read on iOS.
// If there are configured hosts, it means that we actally already have the permission even though PlatformPermissions thinks we wouldn't...
// So skipping the check in that case for now (1.6)
|| configuredHostsModel.count > 0)
}
readonly property alias pageStack: _pageStack
StackView {
id: _pageStack
objectName: "pageStack"
anchors.fill: parent
initialItem: Page {}
}
Component.onCompleted: {
if (configuredHost.uuid.toString() !== "{00000000-0000-0000-0000-000000000000}") {
print("Configured host id is", configuredHost.uuid)
var cachedHost = nymeaDiscovery.nymeaHosts.find(configuredHost.uuid);
if (cachedHost) {
engine.jsonRpcClient.connectToHost(cachedHost)
return;
}
console.warn("There is a last connected host but UUID is unknown to discovery...")
} else if (autoConnectHost.length > 0 && index === 0) {
var host = nymeaDiscovery.nymeaHosts.createLanHost(Configuration.systemName, autoConnectHost);
engine.jsonRpcClient.connectToHost(host)
return;
} else {
// Only hide the splash right away if we're not trying to connect to something
// If it's not hidden here it will be hidden in 3 seconds or when the connection is up, whichever comes first
PlatformHelper.hideSplashScreen();
}
pageStack.push(Configuration.connectionWizard, StackView.Immediate)
}
Timer { running: true; repeat: false; interval: 3000; onTriggered: PlatformHelper.hideSplashScreen(); }
function init() {
print("calling init. Auth required:", engine.jsonRpcClient.authenticationRequired, "initial setup required:", engine.jsonRpcClient.initialSetupRequired, "jsonrpc connected:", engine.jsonRpcClient.connected, "Current host:", engine.jsonRpcClient.currentHost)
pageStack.clear()
// var page = pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml"));
// return
if (!engine.jsonRpcClient.currentHost) {
print("pushing ConnectPage")
pageStack.push(Configuration.connectionWizard)
PlatformHelper.hideSplashScreen();
return;
}
if (engine.jsonRpcClient.authenticationRequired || engine.jsonRpcClient.initialSetupRequired) {
PlatformHelper.hideSplashScreen();
if (engine.jsonRpcClient.pushButtonAuthAvailable) {
print("opening push button auth")
var page = pageStack.push(Qt.resolvedUrl("PushButtonAuthPage.qml"))
page.backPressed.connect(function() {
engine.jsonRpcClient.disconnectFromHost();
init();
})
return;
} else {
if (engine.jsonRpcClient.initialSetupRequired) {
var page = pageStack.push(Qt.resolvedUrl("connection/SetupWizard.qml"));
page.backPressed.connect(function() {
engine.jsonRpcClient.disconnectFromHost()
init();
})
return;
}
var page = pageStack.push(Qt.resolvedUrl("connection/LoginPage.qml"));
page.backPressed.connect(function() {
engine.jsonRpcClient.disconnectFromHost()
init();
})
return;
}
}
if (engine.jsonRpcClient.connected) {
print("Connected to", engine.jsonRpcClient.currentHost.uuid, engine.jsonRpcClient.currentHost.name)
if (Configuration.alternativeMainPage !== "") {
print("Loading alternative main page:", Configuration.alternativeMainPage)
pageStack.push(Qt.resolvedUrl(Configuration.alternativeMainPage))
} else {
pageStack.push(Qt.resolvedUrl("MainPage.qml"))
}
return;
}
var page = pageStack.push(Qt.resolvedUrl("connection/ConnectingPage.qml"));
page.cancel.connect(function(){
engine.jsonRpcClient.disconnectFromHost();
})
}
function handleAndroidBackButton() {
// If we're connected, allow going back up to MainPage
if ((engine.jsonRpcClient.connected && pageStack.depth > 1)
// if we're not connected, only allow using the back button in wizards
|| (!engine.jsonRpcClient.connected && pageStack.depth > 3)) {
pageStack.pop();
return true;
}
return false;
}
// New, nymea thing based push notifactions
function updatePushNotificationThings() {
if (PushNotifications.service == "") {
print("This platform does not support push notifications")
return;
}
if (!PushNotifications.token) {
print("No push notification token available at this time. Not updating...");
return;
}
var clientId = PushNotifications.clientId + "+" + Configuration.appId
print("Updating push notifications")
print("Own push service:", PushNotifications.service);
print("Own client ID:", clientId);
print("Current token:", PushNotifications.token);
for (var i = 0; i < engine.thingManager.things.count; i++) {
var thing = engine.thingManager.things.get(i);
if (thing.thingClass.id.toString().match(/\{?f0dd4c03-0aca-42cc-8f34-9902457b05de\}?/)) {
var serviceParam = thing.paramByName("service");
var clientIdParam = thing.paramByName("clientId")
var tokenParam = thing.paramByName("token")
print("Found a push notification thing for client id:", clientIdParam.value)
if (clientIdParam.value === clientId) {
if (PlatformPermissions.notificationsPermission !== PlatformPermissions.PermissionStatusGranted) {
PlatformPermissions.requestPermission(PlatformPermissions.PermissionNotifications)
}
if (tokenParam.value !== PushNotifications.token) {
var params = [
{ "paramTypeId": serviceParam.paramTypeId, "value": PushNotifications.service },
{ "paramTypeId": clientIdParam.paramTypeId, "value": clientId },
{ "paramTypeId": tokenParam.paramTypeId, "value": PushNotifications.token }
];
print("Reconfiguring PushNotifications for", thing.name)
engine.thingManager.reconfigureThing(thing.id, params);
} else {
print("Push notifications don't need to be updated. Token is valid.")
}
}
}
}
}
Connections {
target: engine.jsonRpcClient
onCurrentHostChanged: {
init();
}
onVerifyConnectionCertificate: {
print("Asking user to verify certificate:", serverUuid, issuerInfo, pem)
var certDialogComponent = Qt.createComponent(Qt.resolvedUrl("connection/CertificateErrorDialog.qml"));
var popup = certDialogComponent.createObject(root);
popup.accepted.connect(function(){
engine.jsonRpcClient.acceptCertificate(serverUuid, pem);
engine.jsonRpcClient.connectToHost(nymeaDiscovery.nymeaHosts.find(serverUuid));
})
popup.open();
}
onConnectedChanged: {
print("json client connected changed", engine.jsonRpcClient.connected, engine.jsonRpcClient.serverUuid)
if (engine.jsonRpcClient.connected) {
nymeaDiscovery.cacheHost(engine.jsonRpcClient.currentHost)
configuredHost.uuid = engine.jsonRpcClient.serverUuid
for (var i = 0; i < configuredHostsModel.count; i++) {
if (i != index && configuredHostsModel.get(i).uuid == engine.jsonRpcClient.serverUuid) {
configuredHostsModel.removeHost(i);
break;
}
}
}
init();
}
onAuthenticationRequiredChanged: {
print("auth required changed")
init();
}
onInitialSetupRequiredChanged: {
print("setup required changed")
init();
}
onInvalidMinimumVersion: {
var popup = invalidVersionComponent.createObject(app.contentItem);
popup.actualVersion = actualVersion;
popup.minVersion = minVersion;
popup.open()
}
onInvalidMaximumVersion: {
var popup = invalidVersionComponent.createObject(app.contentItem);
popup.actualVersion = actualVersion;
popup.maxVersion = maxVersion;
popup.open()
}
onInvalidServerUuid: {
var connection = engine.jsonRpcClient.currentConnection;
engine.jsonRpcClient.disconnectFromHost();
engine.jsonRpcClient.currentHost.connections.removeConnection(connection);
nymeaDiscovery.cacheHost(engine.jsonRpcClient.currentHost);
}
}
Connections {
target: engine.nymeaConfiguration
onFetchingDataChanged: {
print("fetching NymeaConfigration:", engine.nymeaConfiguration.fetchingData)
if (!engine.nymeaConfiguration.fetchingData) {
syncRemoteConnection()
}
}
}
Connections {
target: engine.nymeaConfiguration.tunnelProxyServerConfigurations
onCountChanged: {
print("tunnel proxy count changed:", engine.nymeaConfiguration.tunnelProxyServerConfigurations.count)
if (!engine.nymeaConfiguration.fetchingData) {
syncRemoteConnection();
}
}
}
function syncRemoteConnection() {
if (engine.jsonRpcClient.currentConnection.url.toString().startsWith("tunnel")) {
// Not resyncing tunnel configurations while we're connected through a tunnel ourselves
// (We could, maybe even should, but currently libnymea-app borrows the "NymeaHost" pointer from
// the hostsmodel and will crash if we delete the used Connection object)
return;
}
for (var i = 0; i < engine.jsonRpcClient.currentHost.connections.count; i++) {
var connection = engine.jsonRpcClient.currentHost.connections.get(i)
if (connection.url.toString().startsWith("tunnel")) {
console.log("Removing tunnel proxy connection:", connection.url)
engine.jsonRpcClient.currentHost.connections.removeConnection(i--);
}
}
for (var i = 0; i < engine.nymeaConfiguration.tunnelProxyServerConfigurations.count; i++) {
var tunnelProxyConfig = engine.nymeaConfiguration.tunnelProxyServerConfigurations.get(i);
console.debug("tunnelProxyConfig:", JSON.stringify(tunnelProxyConfig))
var url = tunnelProxyConfig.sslEnabled ? "tunnels://" : "tunnel://";
url += tunnelProxyConfig.address
url += ":" + tunnelProxyConfig.port
url += "?uuid=" + engine.jsonRpcClient.currentHost.uuid
console.info("Adding tunnel proxy connection:", url)
engine.jsonRpcClient.currentHost.connections.addConnection(url, Connection.BearerTypeCloud, tunnelProxyConfig.sslEnabled, "Remote proxy connection", true);
}
nymeaDiscovery.cacheHost(engine.jsonRpcClient.currentHost)
}
Connections {
target: Qt.application
enabled: engine.jsonRpcClient.connected && settings.returnToHome
onStateChanged: {
print("App active state changed:", state)
if (state !== Qt.ApplicationActive) {
init();
}
}
}
Connections {
target: engine.thingManager
onFetchingDataChanged: {
if (!engine.thingManager.fetchingData) {
processPendingPushNotificationActions();
updatePushNotificationThings()
}
PlatformHelper.hideSplashScreen();
}
}
Connections {
target: PlatformHelper
onPendingNotificationActionsChanged: {
processPendingPushNotificationActions()
}
}
function processPendingPushNotificationActions() {
print("pending notification actions changed:", PlatformHelper.pendingNotificationActions)
if (PlatformHelper.pendingNotificationActions.length > 0) {
var notificationAction = PlatformHelper.pendingNotificationActions[0]
if (notificationAction.serverUuid.replace(/[{]]/g, "") !== engine.jsonRpcClient.serverUuid.toString().replace(/[{}]/g, "")) {
print("notification action for different server")
return;
}
print("handling action", JSON.stringify(notificationAction))
if (notificationAction.dataMap.hasOwnProperty("open")) {
// It could be just a thing ID
var target = notificationAction.dataMap["open"]
var thing = engine.thingManager.things.getThing(target)
if (thing) {
print("opening thing:", thing.name)
pageStack.push("/ui/devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces), {thing: thing})
} else {
// or a view name
console.log("going to main view:", target)
pageStack.currentItem.goToView(target, notificationAction.dataMap)
}
}
if (notificationAction.dataMap.hasOwnProperty("execute")) {
var action = notificationAction.dataMap["execute"]
var thingId = notificationAction.dataMap["thingId"]
var actionParams = JSON.parse(notificationAction.dataMap["actionParams"])
print("executing:", thingId, action, actionParams)
engine.thingManager.things.getThing(thingId).executeAction(action, actionParams);
}
PlatformHelper.notificationActionHandled(notificationAction.id)
}
}
Component {
id: invalidVersionComponent
Popup {
id: popup
property string actualVersion: ""
property string minVersion: ""
property string maxVersion: ""
width: app.width * .8
height: col.childrenRect.height + app.margins * 2
x: (app.width - width) / 2
y: (app.height - height) / 2
visible: false
ColumnLayout {
id: col
anchors { left: parent.left; right: parent.right }
spacing: app.margins
Label {
text: qsTr("Connection error")
Layout.fillWidth: true
font.pixelSize: app.largeFont
}
Label {
text: popup.minVersion != ""
? qsTr("The version of the %1 system you are trying to connect to is too old. This app requires at least API version %2 but this %1 system only supports API version %3. Please update your %1 system.").arg(Configuration.systemName).arg(popup.minVersion).arg(popup.actualVersion)
: qsTr("The version of the %1 system you are trying to connect to is too new. This app supports only up to API version %2 but this %1 system provides API version %3. Please update %4.").arg(Configuration.systemName).arg(popup.maxVersion).arg(popup.actualVersion).arg(Configuration.appName)
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Button {
Layout.fillWidth: true
text: qsTr("OK")
onClicked: {
engine.jsonRpcClient.disconnectFromHost();
popup.close()
}
}
}
}
}
}
}
}
}
}