/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 . * * 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.8 import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 import Qt.labs.settings 1.0 import Qt.labs.folderlistmodel 2.2 import Nymea 1.0 import "components" import "delegates" import "mainviews" Page { id: root header: FancyHeader { id: mainHeader title: d.configOverlay !== null ? qsTr("Configure main view") : filteredContentModel.data(swipeView.currentIndex, "displayName") leftButtonVisible: true leftButtonImageSource: { switch (engine.jsonRpcClient.currentConnection.bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "../images/network-wired.svg" } return "../images/network-wifi.svg"; case Connection.BearerTypeBluetooth: return "../images/network-wifi.svg"; case Connection.BearerTypeCloud: return "../images/cloud.svg" case Connection.BearerTypeLoopback: return "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) } return "" } onLeftButtonClicked: { var dialog = connectionDialogComponent.createObject(root) dialog.open(); } onMenuOpenChanged: { if (menuOpen && d.configOverlay) { d.configOverlay.destroy() } } model: ListModel { ListElement { iconSource: "../images/things.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" } ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" } ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" } ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" } } onClicked: { pageStack.push(model.get(index).page) } } Connections { target: engine.ruleManager onAddRuleReply: { d.editRulePage.busy = false if (d.editRulePage) { pageStack.pop(); d.editRulePage = null } } } QtObject { id: d property var editRulePage: null property var configOverlay: null } Settings { id: mainViewSettings category: engine.jsonRpcClient.currentHost.uuid property string mainMenuContent: "" property var sortOrder: [] property var filterList: ["things"] property int currentIndex: 0 } ListModel { id: mainMenuBaseModel // TODO: Should read this from disk somehow maybe? ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things" } ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred" } ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "view-grid-symbolic" } ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow" } ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100" } ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter" } ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media" } } ListModel { id: mainMenuModel ListElement { name: "dummy"; source: "Dummy"; displayName: ""; icon: "" } Component.onCompleted: { var configList = {} var newList = {} var newItems = 0 for (var i = 0; i < mainMenuBaseModel.count; i++) { var item = mainMenuBaseModel.get(i); var idx = mainViewSettings.sortOrder.indexOf(item.name); if (idx === -1) { newList[newItems++] = item; } else { configList[idx] = item; } } clear(); for (idx in configList) { item = configList[idx]; mainMenuModel.append(item) } for (idx in newList) { item = newList[idx]; mainMenuModel.append(item) } tabBar.currentIndex = Qt.binding(function() { return mainViewSettings.currentIndex; }) swipeView.currentIndex = Qt.binding(function() { return tabBar.currentIndex; }) mainViewSettings.currentIndex = Qt.binding(function() { return swipeView.currentIndex; }) } } SortFilterProxyModel { id: filteredContentModel sourceModel: mainMenuModel filterList: mainViewSettings.filterList filterRoleName: "name" } ColumnLayout { id: mainColumn anchors.fill: parent spacing: 0 Pane { Layout.fillWidth: true Layout.preferredHeight: shownHeight property int shownHeight: shown ? contentRow.implicitHeight : 0 property bool shown: updatesModel.count > 0 || engine.systemController.updateRunning visible: shownHeight > 0 Behavior on shownHeight { NumberAnimation { easing.type: Easing.InOutQuad; duration: 150 } } Material.elevation: 2 padding: 0 MouseArea { anchors.fill: parent onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) } Rectangle { color: app.accentColor anchors.fill: parent PackagesFilterModel { id: updatesModel packages: engine.systemController.packages updatesOnly: true } RowLayout { id: contentRow anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: app.margins; rightMargin: app.margins } Item { Layout.fillWidth: true height: app.iconSize } Label { text: engine.systemController.updateRunning ? qsTr("System update in progress...") : qsTr("%n system update(s) available", "", updatesModel.count) color: "white" font.pixelSize: app.smallFont } ColorIcon { height: app.iconSize / 2 width: height color: "white" name: "../images/system-update.svg" RotationAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; running: engine.systemController.updateRunning } } } } } Item { id: contentContainer Layout.fillWidth: true Layout.fillHeight: true clip: true SwipeView { id: swipeView anchors.fill: parent opacity: d.configOverlay === null ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } Repeater { model: d.configOverlay != null ? null : filteredContentModel delegate: Loader { width: swipeView.width height: swipeView.height clip: true source: "mainviews/" + model.source + ".qml" } } } ColumnLayout { anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: app.margins } spacing: app.margins visible: engine.deviceManager.fetchingData BusyIndicator { Layout.alignment: Qt.AlignHCenter running: parent.visible } Label { text: qsTr("Loading data...") font.pixelSize: app.largeFont Layout.fillWidth: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter } } } } footer: Item { readonly property bool shown: tabsRepeater.count > 1 || mainHeader.menuOpen || d.configOverlay implicitHeight: shown ? 70 + (app.landscape ? -20 : 0) : 0 Behavior on implicitHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }} clip: true TabBar { id: tabBar anchors.fill: parent Material.elevation: 3 position: TabBar.Footer visible: !mainHeader.menuOpen && !d.configOverlay opacity: d.configOverlay === null ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } Repeater { id: tabsRepeater model: d.configOverlay != null ? null : filteredContentModel delegate: MainPageTabButton { alignment: app.landscape ? Qt.Horizontal : Qt.Vertical height: tabBar.height anchors.verticalCenter: parent.verticalCenter text: model.displayName iconSource: "../images/" + model.icon + ".svg" onPressAndHold: { PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) d.configOverlay = configComponent.createObject(contentContainer) mainHeader.menuOpen = false; } } } } MainPageTabButton { anchors.fill: parent alignment: app.landscape ? Qt.Horizontal : Qt.Vertical text: d.configOverlay ? qsTr("Done") : qsTr("Configure") iconSource: "../images/configure.svg" opacity: visible ? 1 : 0 visible: mainHeader.menuOpen || d.configOverlay Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } checked: false checkable: false onClicked: { if (d.configOverlay) { d.configOverlay.destroy() } else { PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) d.configOverlay = configComponent.createObject(contentContainer) mainHeader.menuOpen = false; } } } } Component { id: configComponent Item { id: configOverlay width: contentContainer.width height: contentContainer.height NumberAnimation { target: configOverlay property: "scale" duration: 200 easing.type: Easing.InOutQuad from: 2 to: 1 running: true } NumberAnimation { target: configOverlay property: "opacity" duration: 200 easing.type: Easing.InOutQuad from: 0 to: 1 running: true } ListView { id: configListView model: mainMenuModel width: parent.width height: parent.height / 2.5 anchors.centerIn: parent orientation: ListView.Horizontal moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 200 } } property int delegateWidth: width / 2.5 property bool dragging: draggingIndex >= 0 property int draggingIndex : -1 MouseArea { id: dndArea anchors.fill: parent preventStealing: configListView.dragging property int dragOffset: 0 onPressAndHold: { mouse.accepted = true var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; configListView.draggingIndex = configListView.indexAt(mouseXInListView, mouseY) var item = mainMenuModel.get(configListView.draggingIndex) dndItem.displayName = item.displayName dndItem.icon = item.icon var visualItem = configListView.itemAt(mouseXInListView, mouseY) dndItem.isEnabled = visualItem.isEnabled dndArea.dragOffset = configListView.mapToItem(visualItem, mouseX, mouseY).x PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) } onMouseYChanged: { if (configListView.dragging) { var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; var indexUnderMouse = configListView.indexAt(mouseXInListView - dndArea.dragOffset / 2, mouseY) indexUnderMouse = Math.min(Math.max(0, indexUnderMouse), configListView.count - 1) if (configListView.draggingIndex !== indexUnderMouse) { PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) mainMenuModel.move(configListView.draggingIndex, indexUnderMouse, 1) configListView.draggingIndex = indexUnderMouse; } } } onReleased: { print("released!") var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; var clickedIndex = configListView.indexAt(mouseXInListView, mouseY) var item = mainMenuModel.get(clickedIndex) var isEnabled = mainViewSettings.filterList.indexOf(item.name) >= 0; if (!configListView.dragging) { var newList = [] for (var i = 0; i < mainMenuModel.count; i++) { var entry = mainMenuModel.get(i).name; if (entry === item.name) { if (!isEnabled) { newList.push(item.name) } } else { if (mainViewSettings.filterList.indexOf(entry) >= 0) { newList.push(entry) } } } if (newList.length === 0) { newList.push("things") } mainViewSettings.filterList = newList } configListView.draggingIndex = -1; var newSortOrder = [] for (var i = 0; i < mainMenuModel.count; i++) { newSortOrder.push(mainMenuModel.get(i).name) } mainViewSettings.sortOrder = newSortOrder; } Timer { id: scroller interval: 2 repeat: true running: direction != 0 property int direction: { if (!configListView.dragging) { return 0; } return dndArea.mouseX < 50 ? -2 : dndArea.mouseX > dndArea.width - 50 ? 2 : 0 } onTriggered: { configListView.contentX = Math.min(Math.max(0, configListView.contentX + direction), configListView.contentWidth - configListView.width) } } } delegate: Item { id: configDelegate width: configListView.delegateWidth height: configListView.height property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0 visible: configListView.draggingIndex !== index Pane { anchors.fill: parent anchors.margins: app.margins / 2 Material.elevation: 2 leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 contentItem: ItemDelegate { anchors.fill: parent padding: app.margins * 2 contentItem: GridLayout { columns: 1 Item { Layout.fillWidth: true Layout.fillHeight: true ColorIcon { anchors.centerIn: parent width: Math.min(parent.width, parent.height) * .8 height: width name: Qt.resolvedUrl("images/" + model.icon + ".svg") color: configDelegate.isEnabled ? app.accentColor : keyColor } } Label { text: model.displayName Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font.pixelSize: app.largeFont } } } } } Item { id: dndItem width: configListView.delegateWidth height: configListView.height property bool isEnabled: false property string displayName: "" property string icon: "things" visible: configListView.dragging x: dndArea.mouseX - dndArea.dragOffset onVisibleChanged: { if (visible) { dragStartAnimation.start(); } } NumberAnimation { id: dragStartAnimation target: dndItem property: "scale" from: 1 to: 0.9 duration: 200 } Pane { anchors.fill: parent anchors.margins: app.margins / 2 Material.elevation: 2 leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 contentItem: ItemDelegate { anchors.fill: parent padding: app.margins * 2 contentItem: GridLayout { columns: 1 Item { Layout.fillWidth: true Layout.fillHeight: true ColorIcon { anchors.centerIn: parent width: Math.min(parent.width, parent.height) * .8 height: width name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg") color: dndItem.isEnabled ? app.accentColor : keyColor } } Label { text: dndItem.displayName Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font.pixelSize: app.largeFont } } } } } } } } Component { id: connectionDialogComponent MeaDialog { id: connectionDialog title: engine.jsonRpcClient.currentHost.name standardButtons: Dialog.NoButton headerIcon: { switch (engine.jsonRpcClient.currentConnection.bearerType) { case Connection.BearerTypeLan: case Connection.BearerTypeWan: if (engine.jsonRpcClient.availableBearerTypes & NymeaConnection.BearerTypeEthernet != NymeaConnection.BearerTypeNone) { return "../images/network-wired.svg" } return "../images/network-wifi.svg"; case Connection.BearerTypeBluetooth: return "../images/network-wifi.svg"; case Connection.BearerTypeCloud: return "../images/cloud.svg" case Connection.BearerTypeLoopback: return "../images/network-wired.svg" } return "" } Label { Layout.fillWidth: true text: qsTr("Connected to") font.pixelSize: app.smallFont elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentHost.name elide: Text.ElideRight wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter } Item { Layout.fillWidth: true Layout.preferredHeight: app.margins } RowLayout { ColumnLayout { Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentHost.uuid font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) // horizontalAlignment: Text.AlignHCenter } Label { Layout.fillWidth: true text: engine.jsonRpcClient.currentConnection.url font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) // horizontalAlignment: Text.AlignHCenter } } ColorIcon { Layout.preferredHeight: app.iconSize Layout.preferredWidth: app.iconSize name: engine.jsonRpcClient.currentConnection.secure ? "../images/lock-closed.svg" : "../images/lock-open.svg" MouseArea { anchors.fill: parent onClicked: { var component = Qt.createComponent(Qt.resolvedUrl("connection/CertificateDialog.qml")); var popup = component.createObject(app, {serverUuid: engine.jsonRpcClient.serverUuid, issuerInfo: engine.jsonRpcClient.certificateIssuerInfo}); popup.open(); } } } } Item { Layout.fillWidth: true Layout.preferredHeight: app.margins } RowLayout { Layout.fillWidth: true Button { id: disconnectButton text: qsTr("Disconnect") Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) onClicked: { tabSettings.lastConnectedHost = ""; engine.jsonRpcClient.disconnectFromHost(); } } Item { Layout.fillWidth: true } Button { id: cancelButton text: qsTr("OK") Layout.preferredWidth: Math.max(cancelButton.implicitWidth, disconnectButton.implicitWidth) onClicked: connectionDialog.close() } } } } }