Rework main view
This commit is contained in:
parent
7d7225e9db
commit
57d2986948
@ -76,7 +76,7 @@ void DeviceManager::init()
|
||||
qWarning() << "received an event from a device we don't know..." << deviceId << event;
|
||||
return;
|
||||
}
|
||||
qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
|
||||
// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
|
||||
dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
|
||||
emit eventTriggered(deviceId.toString(), eventTypeId.toString(), event.value("params").toMap());
|
||||
});
|
||||
@ -231,7 +231,7 @@ void DeviceManager::notificationReceived(const QVariantMap &data)
|
||||
qWarning() << "received an event from a device we don't know..." << deviceId << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
|
||||
return;
|
||||
}
|
||||
qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
|
||||
// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
|
||||
dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
|
||||
} else if (notification == "Integrations.IOConnectionAdded") {
|
||||
QVariantMap connectionMap = data.value("params").toMap().value("ioConnection").toMap();
|
||||
@ -750,7 +750,7 @@ void DeviceManager::executeBrowserItemActionResponse(const QVariantMap ¶ms)
|
||||
|
||||
void DeviceManager::getIOConnectionsResponse(const QVariantMap ¶ms)
|
||||
{
|
||||
qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
||||
// qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
||||
|
||||
foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) {
|
||||
QVariantMap connectionMap = connectionVariant.toMap();
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
#include "ruletemplates/ruleactionparamtemplate.h"
|
||||
#include "connection/awsclient.h"
|
||||
#include "models/devicemodel.h"
|
||||
#include "models/sortfilterproxymodel.h"
|
||||
#include "system/systemcontroller.h"
|
||||
#include "types/package.h"
|
||||
#include "types/packages.h"
|
||||
@ -177,8 +178,11 @@ void registerQmlTypes() {
|
||||
qmlRegisterType<VendorsProxy>(uri, 1, 0, "VendorsProxy");
|
||||
|
||||
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Device", "Can't create this in QML. Get it from the Devices.");
|
||||
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Thing", "Can't create this in QML. Get it from the Things.");
|
||||
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager.");
|
||||
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Things", "Can't create this in QML. Get it from the ThingManager.");
|
||||
qmlRegisterType<DevicesProxy>(uri, 1, 0, "DevicesProxy");
|
||||
qmlRegisterType<DevicesProxy>(uri, 1, 0, "ThingsProxy");
|
||||
qmlRegisterType<InterfacesModel>(uri, 1, 0, "InterfacesModel");
|
||||
qmlRegisterType<InterfacesSortModel>(uri, 1, 0, "InterfacesSortModel");
|
||||
|
||||
@ -311,6 +315,8 @@ void registerQmlTypes() {
|
||||
qmlRegisterUncreatableType<IOConnection>(uri, 1, 0, "IOConnection", "Get it from IOConnections");
|
||||
qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher");
|
||||
qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher");
|
||||
|
||||
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
|
||||
}
|
||||
|
||||
#endif // LIBNYMEAAPPCORE_H
|
||||
|
||||
@ -27,6 +27,7 @@ INCLUDEPATH += $$top_srcdir/QtZeroConf
|
||||
SOURCES += \
|
||||
configuration/networkmanager.cpp \
|
||||
engine.cpp \
|
||||
models/sortfilterproxymodel.cpp \
|
||||
ruletemplates/calendaritemtemplate.cpp \
|
||||
ruletemplates/timedescriptortemplate.cpp \
|
||||
ruletemplates/timeeventitemtemplate.cpp \
|
||||
@ -162,6 +163,7 @@ SOURCES += \
|
||||
HEADERS += \
|
||||
configuration/networkmanager.h \
|
||||
engine.h \
|
||||
models/sortfilterproxymodel.h \
|
||||
ruletemplates/calendaritemtemplate.h \
|
||||
ruletemplates/timedescriptortemplate.h \
|
||||
ruletemplates/timeeventitemtemplate.h \
|
||||
|
||||
62
libnymea-app/models/sortfilterproxymodel.cpp
Normal file
62
libnymea-app/models/sortfilterproxymodel.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
#include "sortfilterproxymodel.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
SortFilterProxyModel::SortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(this, &QSortFilterProxyModel::sourceModelChanged, this, [=](){
|
||||
connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterProxyModel::countChanged);
|
||||
connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterProxyModel::countChanged);
|
||||
connect(sourceModel(), &QAbstractItemModel::modelReset, this, &SortFilterProxyModel::countChanged);
|
||||
emit countChanged();
|
||||
});
|
||||
}
|
||||
|
||||
QString SortFilterProxyModel::filterRoleName() const
|
||||
{
|
||||
return m_filterRoleName;
|
||||
}
|
||||
|
||||
void SortFilterProxyModel::setFilterRoleName(const QString &filterRoleName)
|
||||
{
|
||||
if (m_filterRoleName != filterRoleName) {
|
||||
m_filterRoleName = filterRoleName;
|
||||
emit filterRoleNameChanged();
|
||||
invalidateFilter();
|
||||
emit countChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList SortFilterProxyModel::filterList() const
|
||||
{
|
||||
return m_filterList;
|
||||
}
|
||||
|
||||
void SortFilterProxyModel::setFilterList(const QStringList &filterList)
|
||||
{
|
||||
if (m_filterList != filterList) {
|
||||
m_filterList = filterList;
|
||||
emit filterListChanged();
|
||||
invalidateFilter();
|
||||
emit countChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant SortFilterProxyModel::data(int row, const QString &role) const
|
||||
{
|
||||
int roleId = roleNames().key(role.toUtf8());
|
||||
return QSortFilterProxyModel::data(index(row, 0), roleId);
|
||||
}
|
||||
|
||||
bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
if (!m_filterList.isEmpty() && !m_filterRoleName.isEmpty()) {
|
||||
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
|
||||
int filterRole = sourceModel()->roleNames().key(m_filterRoleName.toUtf8());
|
||||
QVariant data = sourceModel()->data(idx, filterRole);
|
||||
if (!m_filterList.contains(data.toString())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
|
||||
}
|
||||
37
libnymea-app/models/sortfilterproxymodel.h
Normal file
37
libnymea-app/models/sortfilterproxymodel.h
Normal file
@ -0,0 +1,37 @@
|
||||
#ifndef SORTFILTERPROXYMODEL_H
|
||||
#define SORTFILTERPROXYMODEL_H
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class SortFilterProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString filterRoleName READ filterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged)
|
||||
Q_PROPERTY(QStringList filterList READ filterList WRITE setFilterList NOTIFY filterListChanged)
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
explicit SortFilterProxyModel(QObject *parent = nullptr);
|
||||
|
||||
QString filterRoleName() const;
|
||||
void setFilterRoleName(const QString &filterRoleName);
|
||||
|
||||
QStringList filterList() const;
|
||||
void setFilterList(const QStringList &filterList);
|
||||
|
||||
Q_INVOKABLE QVariant data(int row, const QString &role) const;
|
||||
|
||||
signals:
|
||||
void filterRoleNameChanged();
|
||||
void filterListChanged();
|
||||
void countChanged();
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
private:
|
||||
QString m_filterRoleName;
|
||||
QStringList m_filterList;
|
||||
};
|
||||
|
||||
#endif // SORTFILTERPROXYMODEL_H
|
||||
@ -129,7 +129,7 @@
|
||||
<file>ui/images/send.svg</file>
|
||||
<file>ui/images/sensors.svg</file>
|
||||
<file>ui/images/settings.svg</file>
|
||||
<file>ui/images/share.svg</file>
|
||||
<file>ui/images/things.svg</file>
|
||||
<file>ui/images/slideshow.svg</file>
|
||||
<file>ui/images/closable-move.svg</file>
|
||||
<file>ui/images/starred.svg</file>
|
||||
@ -234,5 +234,6 @@
|
||||
<file>ui/images/garage/garage-100.svg</file>
|
||||
<file>ui/images/navigationpad.svg</file>
|
||||
<file>ui/images/qrcode.svg</file>
|
||||
<file>ui/images/energy.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
12
nymea-app/mainmenumodel.cpp
Normal file
12
nymea-app/mainmenumodel.cpp
Normal file
@ -0,0 +1,12 @@
|
||||
#include "mainmenumodel.h"
|
||||
|
||||
MainMenuModel::MainMenuModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
int MainMenuModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_list.count();
|
||||
}
|
||||
24
nymea-app/mainmenumodel.h
Normal file
24
nymea-app/mainmenumodel.h
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef MAINMENUMODEL_H
|
||||
#define MAINMENUMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class MainMenuItem: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
};
|
||||
|
||||
class MainMenuModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainMenuModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
private:
|
||||
QList<MainMenuItem*> m_list;
|
||||
};
|
||||
|
||||
#endif // MAINMENUMODEL_H
|
||||
@ -13,6 +13,7 @@ linux:!android:!nozeroconf:LIBS += -lavahi-client -lavahi-common
|
||||
PRE_TARGETDEPS += ../libnymea-app
|
||||
|
||||
HEADERS += \
|
||||
mainmenumodel.h \
|
||||
platformintegration/generic/raspberrypihelper.h \
|
||||
stylecontroller.h \
|
||||
pushnotifications.h \
|
||||
@ -22,6 +23,7 @@ HEADERS += \
|
||||
ruletemplates/messages.h
|
||||
|
||||
SOURCES += main.cpp \
|
||||
mainmenumodel.cpp \
|
||||
platformintegration/generic/raspberrypihelper.cpp \
|
||||
stylecontroller.cpp \
|
||||
pushnotifications.cpp \
|
||||
|
||||
@ -10,8 +10,7 @@
|
||||
<file>ui/RootItem.qml</file>
|
||||
<file>ui/mainviews/ScenesView.qml</file>
|
||||
<file>ui/mainviews/FavoritesView.qml</file>
|
||||
<file>ui/mainviews/DevicesPageDelegate.qml</file>
|
||||
<file>ui/mainviews/DevicesPage.qml</file>
|
||||
<file>ui/mainviews/ThingsView.qml</file>
|
||||
<file>ui/connection/ConnectPage.qml</file>
|
||||
<file>ui/connection/ManualConnectPage.qml</file>
|
||||
<file>ui/connection/ConnectingPage.qml</file>
|
||||
@ -108,6 +107,7 @@
|
||||
<file>ui/delegates/ParamDelegate.qml</file>
|
||||
<file>ui/delegates/ActionDelegate.qml</file>
|
||||
<file>ui/delegates/ThingDelegate.qml</file>
|
||||
<file>ui/delegates/InterfaceTile.qml</file>
|
||||
<file>ui/system/LogViewerPage.qml</file>
|
||||
<file>ui/system/PluginsPage.qml</file>
|
||||
<file>ui/system/PluginParamsPage.qml</file>
|
||||
@ -218,5 +218,9 @@
|
||||
<file>ui/thingconfiguration/ThingClassDetailsPage.qml</file>
|
||||
<file>ui/components/ClosablesControlLarge.qml</file>
|
||||
<file>ui/devicepages/BarcodeScannerThingPage.qml</file>
|
||||
<file>ui/mainviews/GaragesView.qml</file>
|
||||
<file>ui/mainviews/EnergyView.qml</file>
|
||||
<file>ui/components/MainViewBase.qml</file>
|
||||
<file>ui/components/SmartMeterChart.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -33,6 +33,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"
|
||||
@ -42,7 +44,8 @@ Page {
|
||||
id: root
|
||||
|
||||
header: FancyHeader {
|
||||
title: swipeView.currentItem.title
|
||||
id: mainHeader
|
||||
title: filteredContentModel.data(swipeView.currentIndex, "displayName")
|
||||
leftButtonVisible: true
|
||||
leftButtonImageSource: {
|
||||
switch (engine.jsonRpcClient.currentConnection.bearerType) {
|
||||
@ -65,10 +68,14 @@ Page {
|
||||
var dialog = connectionDialogComponent.createObject(root)
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
onMenuOpenChanged: {
|
||||
if (menuOpen && d.configOverlay) {
|
||||
d.configOverlay.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
model: ListModel {
|
||||
ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" }
|
||||
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" }
|
||||
@ -79,84 +86,6 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
property int currentViewIndex: 0
|
||||
|
||||
property bool swipeViewReady: false
|
||||
property bool tabsReady: false
|
||||
|
||||
// FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there.
|
||||
// Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar
|
||||
// has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex.
|
||||
Component.onCompleted: {
|
||||
// Fill SwipeView (The 2 static views things and scenes will already be there).
|
||||
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
|
||||
swipeView.insertItem(0, favoritesViewComponent.createObject(swipeView))
|
||||
}
|
||||
var experienceView = null;
|
||||
if (styleController.currentExperience != "Default") {
|
||||
experienceView = experienceViewComponent.createObject(swipeView, {source: "experiences/" + styleController.currentExperience + "/Main.qml" });
|
||||
swipeView.insertItem(0, experienceView)
|
||||
}
|
||||
root.swipeViewReady = true;
|
||||
|
||||
|
||||
var pi = 0;
|
||||
if (experienceView) {
|
||||
tabEntryComponent.createObject(tabBar, {text: experienceView.title, iconSource: experienceView.icon, pageIndex: pi++})
|
||||
}
|
||||
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
|
||||
tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++})
|
||||
}
|
||||
tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++})
|
||||
tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++})
|
||||
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
|
||||
tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++})
|
||||
}
|
||||
|
||||
root.tabsReady = true
|
||||
}
|
||||
|
||||
readonly property bool viewReady: swipeViewReady && tabsReady
|
||||
onViewReadyChanged: {
|
||||
if (tabSettings.currentMainViewIndex > swipeView.count) {
|
||||
tabSettings.currentMainViewIndex = swipeView.count - 1;
|
||||
}
|
||||
|
||||
// Load current index from settings
|
||||
currentViewIndex = tabSettings.currentMainViewIndex;
|
||||
|
||||
// If setting is not initialized yet, init to "Things" page (might be 0 or 1, depending whether we have tags support)
|
||||
if (currentViewIndex === -1) {
|
||||
currentViewIndex = engine.jsonRpcClient.ensureServerVersion(1.6) ? 1 : 0
|
||||
}
|
||||
|
||||
// and set up a binding to sync changes back to the settings
|
||||
tabSettings.currentMainViewIndex = Qt.binding(function() { return root.currentViewIndex; });
|
||||
|
||||
// Tabbar gets a little confused if it's bound to it before the init happened, do it now
|
||||
tabBar.currentIndex = Qt.binding(function() { return root.currentViewIndex; });
|
||||
}
|
||||
|
||||
// FIXME: Currently we don't have any feedback for executeAction
|
||||
// we don't want all the results, e.g. on looped calls like "all off"
|
||||
// Connections {
|
||||
// target: engine.deviceManager
|
||||
// onExecuteActionReply: {
|
||||
// var text = params["deviceError"]
|
||||
// switch(text) {
|
||||
// case "DeviceErrorNoError":
|
||||
// return;
|
||||
// case "DeviceErrorHardwareNotAvailable":
|
||||
// text = qsTr("Could not execute action. The thing is not available");
|
||||
// break;
|
||||
// }
|
||||
|
||||
// var errorDialog = Qt.createComponent(Qt.resolvedUrl("components/ErrorDialog.qml"))
|
||||
// var popup = errorDialog.createObject(root, {text: text})
|
||||
// popup.open()
|
||||
// }
|
||||
// }
|
||||
|
||||
Connections {
|
||||
target: engine.ruleManager
|
||||
onAddRuleReply: {
|
||||
@ -170,6 +99,69 @@ Page {
|
||||
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" }
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -226,8 +218,8 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
@ -235,131 +227,17 @@ Page {
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
anchors.fill: parent
|
||||
currentIndex: root.currentViewIndex
|
||||
opacity: d.configOverlay === null ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
root.currentViewIndex = currentIndex
|
||||
}
|
||||
Repeater {
|
||||
model: filteredContentModel
|
||||
|
||||
Component {
|
||||
id: experienceViewComponent
|
||||
Loader {
|
||||
delegate: Loader {
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
clip: true
|
||||
readonly property string title: item ? item.title : ""
|
||||
readonly property string icon: item ? item.icon : ""
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: favoritesViewComponent
|
||||
FavoritesView {
|
||||
id: favoritesView
|
||||
objectName: "favorites"
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
property string title: qsTr("My favorites")
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: favoritesView.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("There are no favorite things yet.")
|
||||
text: engine.deviceManager.devices.count === 0 ?
|
||||
qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") :
|
||||
qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.")
|
||||
imageSource: "images/starred.svg"
|
||||
buttonVisible: engine.deviceManager.devices.count === 0
|
||||
buttonText: qsTr("Add a thing")
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
DevicesPage {
|
||||
property string title: qsTr("My things")
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
model: InterfacesSortModel {
|
||||
interfacesModel: InterfacesModel {
|
||||
engine: _engine
|
||||
devices: DevicesProxy {
|
||||
engine: _engine
|
||||
}
|
||||
shownInterfaces: app.supportedInterfaces
|
||||
showUncategorized: true
|
||||
}
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("Welcome to %1!").arg(app.systemName)
|
||||
// Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice
|
||||
text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName)
|
||||
imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
|
||||
buttonText: qsTr("Add a thing")
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
ScenesView {
|
||||
id: scenesView
|
||||
property string title: qsTr("My scenes");
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: scenesView.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("There are no scenes set up yet.")
|
||||
text: engine.deviceManager.devices.count === 0 ?
|
||||
qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") :
|
||||
qsTr("Scenes provide a useful way to control your things with just one click.")
|
||||
imageSource: "images/slideshow.svg"
|
||||
buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene")
|
||||
onButtonClicked: {
|
||||
if (engine.deviceManager.devices.count === 0) {
|
||||
pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
|
||||
} else {
|
||||
var newRule = engine.ruleManager.createNewRule();
|
||||
d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: newRule });
|
||||
d.editRulePage.startAddAction();
|
||||
d.editRulePage.StackView.onRemoved.connect(function() {
|
||||
newRule.destroy();
|
||||
})
|
||||
d.editRulePage.onAccept.connect(function() {
|
||||
d.editRulePage.busy = true;
|
||||
engine.ruleManager.addRule(d.editRulePage.rule);
|
||||
})
|
||||
d.editRulePage.onCancel.connect(function() {
|
||||
pageStack.pop();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupsView {
|
||||
id: groupsView
|
||||
property string title: qsTr("My groups");
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: groupsView.count == 0 && !engine.deviceManager.fetchingData && !engine.tagsManager.busy
|
||||
title: qsTr("There are no groups set up yet.")
|
||||
text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.")
|
||||
imageSource: "images/view-grid-symbolic.svg"
|
||||
buttonVisible: false
|
||||
// buttonText: qsTr("Create a group")
|
||||
// onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
|
||||
source: "mainviews/" + model.source + ".qml"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -383,19 +261,300 @@ Page {
|
||||
}
|
||||
|
||||
}
|
||||
footer: TabBar {
|
||||
id: tabBar
|
||||
Material.elevation: 3
|
||||
position: TabBar.Footer
|
||||
implicitHeight: 70 + (app.landscape ? -20 : 0)
|
||||
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
|
||||
|
||||
Component {
|
||||
id: tabEntryComponent
|
||||
MainPageTabButton {
|
||||
property int pageIndex: 0
|
||||
// height: tabBar.height
|
||||
onClicked: root.currentViewIndex = pageIndex
|
||||
alignment: app.landscape ? Qt.Horizontal : Qt.Vertical
|
||||
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: 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 ? -1 : dndArea.mouseX > dndArea.width - 50 ? 1 : 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -453,7 +612,7 @@ Page {
|
||||
font.pixelSize: app.smallFont
|
||||
elide: Text.ElideRight
|
||||
color: Material.color(Material.Grey)
|
||||
// horizontalAlignment: Text.AlignHCenter
|
||||
// horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
@ -461,7 +620,7 @@ Page {
|
||||
font.pixelSize: app.smallFont
|
||||
elide: Text.ElideRight
|
||||
color: Material.color(Material.Grey)
|
||||
// horizontalAlignment: Text.AlignHCenter
|
||||
// horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
ColorIcon {
|
||||
|
||||
@ -33,6 +33,7 @@ import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import Qt.labs.settings 1.0
|
||||
import Qt.labs.folderlistmodel 2.2
|
||||
import QtQuick.Window 2.3
|
||||
import Nymea 1.0
|
||||
|
||||
@ -533,6 +534,11 @@ ApplicationWindow {
|
||||
onStateChanged: closeTimer.stop()
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: availableMainViews
|
||||
folder: "mainviews"
|
||||
showFiles: false
|
||||
}
|
||||
|
||||
// NOTE: If using a Dialog, make sure closePolicy does not contain Dialog.CloseOnPressOutside
|
||||
// or the virtual keyboard will close when pressing it...
|
||||
|
||||
@ -48,7 +48,10 @@ Item {
|
||||
id: image
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent ? parent.margins : 0
|
||||
source: width > 0 && height > 0 && icon.name ? icon.name : ""
|
||||
source: width > 0 && height > 0 && icon.name ?
|
||||
icon.name.endsWith(".svg") ? icon.name
|
||||
: "qrc:/ui/images/" + icon.name + ".svg"
|
||||
: ""
|
||||
sourceSize {
|
||||
width: width
|
||||
height: height
|
||||
|
||||
@ -35,7 +35,7 @@ import QtQuick.Controls.Material 2.1
|
||||
|
||||
ToolBar {
|
||||
id: root
|
||||
height: 50 + (d.menuOpen ? app.iconSize * 3 + app.margins / 2 : 0)
|
||||
height: 50 + (menuOpen ? app.iconSize * 3 + app.margins / 2 : 0)
|
||||
Behavior on height { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
|
||||
|
||||
property string title
|
||||
@ -47,16 +47,13 @@ ToolBar {
|
||||
signal clicked(int index);
|
||||
signal leftButtonClicked();
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property bool menuOpen: false
|
||||
}
|
||||
property bool menuOpen: false
|
||||
|
||||
RowLayout {
|
||||
id: mainRow
|
||||
height: 50
|
||||
width: parent.width
|
||||
opacity: d.menuOpen ? 0 : 1
|
||||
opacity: menuOpen ? 0 : 1
|
||||
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
|
||||
|
||||
HeaderButton {
|
||||
@ -81,7 +78,7 @@ ToolBar {
|
||||
HeaderButton {
|
||||
id: menuButton
|
||||
imageSource: "../images/navigation-menu.svg"
|
||||
onClicked: d.menuOpen = true
|
||||
onClicked: menuOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +86,7 @@ ToolBar {
|
||||
height: 50
|
||||
anchors.bottom: menuPanel.top
|
||||
width: parent.width
|
||||
opacity: d.menuOpen ? 1 : 0
|
||||
opacity: menuOpen ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
|
||||
|
||||
@ -106,7 +103,7 @@ ToolBar {
|
||||
|
||||
HeaderButton {
|
||||
imageSource:"../images/close.svg"
|
||||
onClicked: d.menuOpen = false
|
||||
onClicked: menuOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +115,7 @@ ToolBar {
|
||||
width: Math.min(menuRow.childrenRect.width, parent.width)
|
||||
height: app.iconSize * 3
|
||||
contentWidth: menuRow.childrenRect.width
|
||||
opacity: d.menuOpen ? 1 : 0
|
||||
opacity: menuOpen ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
|
||||
|
||||
@ -132,7 +129,7 @@ ToolBar {
|
||||
width: app.iconSize * 3
|
||||
|
||||
onClicked: {
|
||||
d.menuOpen = false
|
||||
menuOpen = false
|
||||
root.clicked(index)
|
||||
}
|
||||
|
||||
|
||||
@ -44,22 +44,27 @@ TabButton {
|
||||
opacity: 0.05
|
||||
}
|
||||
|
||||
contentItem: GridLayout {
|
||||
columns: root.alignment === Qt.Vertical ? 1 : 2
|
||||
rowSpacing: 4
|
||||
ColorIcon {
|
||||
Layout.preferredWidth: app.iconSize
|
||||
Layout.preferredHeight: app.iconSize
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
name: root.iconSource
|
||||
color: root.checked ? app.accentColor : keyColor
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: root.alignment === Qt.Vertical
|
||||
text: root.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: app.smallFont
|
||||
color: root.checked ? app.accentColor : Material.foreground
|
||||
contentItem: Item {
|
||||
height: root.height
|
||||
Grid {
|
||||
anchors.centerIn: parent
|
||||
columns: root.alignment == Qt.Vertical ? 1 : 2
|
||||
spacing: root.alignment == Qt.Horizontal ? app.margins : app.margins / 2
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
verticalItemAlignment: Grid.AlignVCenter
|
||||
|
||||
ColorIcon {
|
||||
width: app.iconSize
|
||||
height: app.iconSize
|
||||
name: root.iconSource
|
||||
color: root.checked ? app.accentColor : keyColor
|
||||
}
|
||||
Label {
|
||||
id: textLabel
|
||||
text: root.text
|
||||
font.pixelSize: app.smallFont
|
||||
color: root.checked ? app.accentColor : Material.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,30 +34,13 @@ import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
import "../delegates"
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
property alias count: interfacesGridView.count
|
||||
property alias model: interfacesGridView.model
|
||||
|
||||
// Prevent scroll events to swipe left/right in case they fall through the grid
|
||||
preventStealing: true
|
||||
onWheel: wheel.accepted = true
|
||||
|
||||
GridView {
|
||||
id: interfacesGridView
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins / 2
|
||||
|
||||
readonly property int minTileWidth: 172
|
||||
readonly property int tilesPerRow: root.width / minTileWidth
|
||||
|
||||
cellWidth: width / tilesPerRow
|
||||
cellHeight: cellWidth
|
||||
delegate: DevicesPageDelegate {
|
||||
width: interfacesGridView.cellWidth
|
||||
height: interfacesGridView.cellHeight
|
||||
iface: Interfaces.findByName(model.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
127
nymea-app/ui/components/SmartMeterChart.qml
Normal file
127
nymea-app/ui/components/SmartMeterChart.qml
Normal file
@ -0,0 +1,127 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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.8
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtCharts 2.2
|
||||
import Nymea 1.0
|
||||
|
||||
ChartView {
|
||||
id: chart
|
||||
backgroundColor: app.backgroundColor
|
||||
theme: ChartView.ChartThemeLight
|
||||
legend.labelColor: app.foregroundColor
|
||||
legend.font.pixelSize: app.smallFont
|
||||
legend.alignment: Qt.AlignRight
|
||||
titleColor: app.foregroundColor
|
||||
|
||||
property ThingsProxy meters: null
|
||||
property int multiplier: 1
|
||||
|
||||
Connections {
|
||||
target: meters
|
||||
onCountChanged: chart.refresh()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
chart.refresh()
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property var sliceMap: {}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
pieSeries.clear();
|
||||
d.sliceMap = {}
|
||||
print("calculating", chart.multiplier)
|
||||
for (var i = 0; i < meters.count; i++) {
|
||||
var thing = meters.get(i);
|
||||
print("thing:", thing.name)
|
||||
var value = 0;
|
||||
var totalConsumedStateType = thing.thingClass.stateTypes.findByName("totalEnergyConsumed")
|
||||
if (totalConsumedStateType) {
|
||||
var totalConsumedState = thing.states.getState(totalConsumedStateType.id)
|
||||
value = value + (totalConsumedState.value * chart.multiplier)
|
||||
print("Adding", totalConsumedState.value * chart.multiplier, value)
|
||||
}
|
||||
var totalProducedStateType = thing.thingClass.stateTypes.findByName("totalEnergyProduced")
|
||||
if (totalProducedStateType) {
|
||||
var totalProducedState = thing.states.getState(totalProducedStateType.id)
|
||||
value = value - (totalProducedState.value * chart.multiplier)
|
||||
print("removing", totalProducedState.value * chart.multiplier, value)
|
||||
}
|
||||
print("consumed", totalConsumedState.value, "produced", totalProducedState.value)
|
||||
print("value", value)
|
||||
var slice = pieSeries.append(thing.name, Math.max(0, value))
|
||||
var color = app.accentColor
|
||||
for (var j = 0; j < i; j+=2) {
|
||||
if (i % 2 == 0) {
|
||||
color = Qt.lighter(color, 1.2);
|
||||
} else {
|
||||
color = Qt.darker(color, 1.2)
|
||||
}
|
||||
}
|
||||
slice.color = color
|
||||
d.sliceMap[slice] = i
|
||||
}
|
||||
}
|
||||
|
||||
PieSeries {
|
||||
id: pieSeries
|
||||
holeSize: 0.6
|
||||
size: 0.8
|
||||
|
||||
onClicked: {
|
||||
print("clicked slice", slice, d.sliceMap[slice], meters.get(d.sliceMap[slice]))
|
||||
pageStack.push("../devicepages/SmartMeterDevicePage.qml", {device: meters.get(d.sliceMap[slice])})
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
x: chart.plotArea.x + (chart.plotArea.width * 0.5) - (width / 2)
|
||||
y: chart.plotArea.y + (chart.plotArea.height * 0.5) - (height / 2)
|
||||
|
||||
Label {
|
||||
font.pixelSize: app.largeFont
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Math.round(pieSeries.sum * 1000) / 1000
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "KWh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ MainPageTile {
|
||||
case "media":
|
||||
return mediaControlComponent
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineControl: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile, inlineControl: Unhandled interface", iface.name)
|
||||
}
|
||||
|
||||
}
|
||||
@ -267,7 +267,7 @@ MainPageTile {
|
||||
if (thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("garagegate") >= 0) {
|
||||
statefulCount++;
|
||||
var stateType = thing.thingClass.stateTypes.findByName("state");
|
||||
if (stateType && device.states.getState(stateType.id).value !== "closed") {
|
||||
if (stateType && thing.states.getState(stateType.id).value !== "closed") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@ -285,7 +285,7 @@ MainPageTile {
|
||||
return ""
|
||||
// return qsTr("%1 installed").arg(devicesProxy.count)
|
||||
}
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl: Unhandled interface", model.name)
|
||||
console.warn("InterfaceTile, inlineButtonControl: Unhandled interface", model.name)
|
||||
}
|
||||
font.pixelSize: app.smallFont
|
||||
elide: Text.ElideRight
|
||||
@ -329,7 +329,7 @@ MainPageTile {
|
||||
case "extendedshutter":
|
||||
return "../images/up.svg"
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile", "inlineButtonControl image: Unhandled interface", iface.name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -370,7 +370,7 @@ MainPageTile {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile:", "inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,7 +409,7 @@ MainPageTile {
|
||||
case "extendedshutter":
|
||||
return "../images/media-playback-stop.svg"
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name)
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@ -450,7 +450,7 @@ MainPageTile {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -500,7 +500,7 @@ MainPageTile {
|
||||
case "extendedshutter":
|
||||
return "../images/down.svg"
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -585,7 +585,7 @@ MainPageTile {
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,12 +71,12 @@ Page {
|
||||
|
||||
cellWidth: width / tilesPerRow
|
||||
cellHeight: cellWidth
|
||||
// delegate: DevicesPageDelegate {
|
||||
// delegate: InterfaceTile {
|
||||
// width: interfacesGridView.cellWidth
|
||||
// height: interfacesGridView.cellHeight
|
||||
// }
|
||||
|
||||
delegate: DevicesPageDelegate {
|
||||
delegate: InterfaceTile {
|
||||
width: interfacesGridView.cellWidth
|
||||
height: interfacesGridView.cellHeight
|
||||
iface: Interfaces.findByName(model.name)
|
||||
|
||||
23
nymea-app/ui/images/energy.svg
Normal file
23
nymea-app/ui/images/energy.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg id="svg4874" width="96" height="96" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata id="metadata4879">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g id="layer1" transform="translate(67.857 -78.505)">
|
||||
<g id="g4845" transform="matrix(0 -1 -1 0 373.51 516.51)">
|
||||
<g id="g4778" transform="matrix(-.9996 0 0 1 575.94 -611)">
|
||||
<g id="g4780" transform="matrix(-1 0 0 1 576 611)">
|
||||
<rect id="rect4782" transform="scale(-1,1)" x="-438" y="345.36" width="96.038" height="96" style="color:#000000;fill:none"/>
|
||||
<path id="path4212" d="m341.96 389 56.75 24v-15.273h39.288l-56.75-24v15.273z" style="fill:#808080"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
91
nymea-app/ui/mainviews/EnergyView.qml
Normal file
91
nymea-app/ui/mainviews/EnergyView.qml
Normal file
@ -0,0 +1,91 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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.8
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtCharts 2.2
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
import "../delegates"
|
||||
|
||||
MainViewBase {
|
||||
id: root
|
||||
|
||||
ThingsProxy {
|
||||
id: consumers
|
||||
engine: _engine
|
||||
shownInterfaces: ["smartmeterconsumer"]
|
||||
}
|
||||
ThingsProxy {
|
||||
id: producers
|
||||
engine: _engine
|
||||
shownInterfaces: ["smartmeterproducer"]
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - app.margins * 2
|
||||
visible: !engine.thingManager.fetchingData && consumers.count == 0
|
||||
title: qsTr("There are no energy meters installed.")
|
||||
text: qsTr("To get an overview of your current energy usage, install some energy meters.")
|
||||
imageSource: "../images/smartmeter.svg"
|
||||
buttonText: qsTr("Add things")
|
||||
}
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: energyGrid.childrenRect.height
|
||||
|
||||
GridLayout {
|
||||
id: energyGrid
|
||||
width: parent.width
|
||||
visible: consumers.count > 0
|
||||
columns: Math.floor(root.width / 300)
|
||||
|
||||
SmartMeterChart {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width * .7
|
||||
meters: consumers
|
||||
title: qsTr("Total consumed energy")
|
||||
visible: consumers.count > 0
|
||||
}
|
||||
SmartMeterChart {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width * .7
|
||||
meters: producers
|
||||
title: qsTr("Total produced energy")
|
||||
visible: producers.count > 0
|
||||
multiplier: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,15 +36,10 @@ import Nymea 1.0
|
||||
import "../components"
|
||||
import "../delegates"
|
||||
|
||||
MouseArea {
|
||||
MainViewBase {
|
||||
id: root
|
||||
|
||||
property bool editMode: false
|
||||
readonly property int count: tagsProxy.count
|
||||
|
||||
// Prevent scroll events to swipe left/right in case they fall through the grid
|
||||
preventStealing: true
|
||||
onWheel: wheel.accepted = true
|
||||
|
||||
TagsProxyModel {
|
||||
id: tagsProxy
|
||||
@ -167,4 +162,19 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: gridView.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("There are no favorite things yet.")
|
||||
text: engine.deviceManager.devices.count === 0 ?
|
||||
qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") :
|
||||
qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.")
|
||||
imageSource: "../images/starred.svg"
|
||||
buttonVisible: engine.deviceManager.devices.count === 0
|
||||
buttonText: qsTr("Add a thing")
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
240
nymea-app/ui/mainviews/GaragesView.qml
Normal file
240
nymea-app/ui/mainviews/GaragesView.qml
Normal file
@ -0,0 +1,240 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls 2.2
|
||||
import "../components"
|
||||
import Nymea 1.0
|
||||
|
||||
MainViewBase {
|
||||
id: root
|
||||
|
||||
readonly property bool landscape: width > height
|
||||
|
||||
DevicesProxy {
|
||||
id: garagesFilterModel
|
||||
engine: _engine
|
||||
shownInterfaces: ["garagedoor", "garagegate"]
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - app.margins * 2
|
||||
text: qsTr("There are no garage doors set up yet.")
|
||||
imageSource: "qrc:/ui/images/garage/garage-100.svg"
|
||||
buttonText: qsTr("Set up now")
|
||||
visible: garagesFilterModel.count === 0 && !engine.thingManager.fetchingData
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
anchors.fill: parent
|
||||
|
||||
Repeater {
|
||||
model: garagesFilterModel
|
||||
|
||||
Item {
|
||||
id: garageGateView
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
|
||||
readonly property Device thing: garagesFilterModel.get(index)
|
||||
|
||||
|
||||
readonly property bool isImpulseBased: thing.thingClass.interfaces.indexOf("impulsegaragedoor") >= 0
|
||||
readonly property bool isStateful: thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0
|
||||
readonly property bool isExtended: thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0
|
||||
|
||||
// Stateful garagedoor
|
||||
readonly property StateType stateStateType: thing.thingClass.stateTypes.findByName("state")
|
||||
readonly property State stateState: stateStateType ? thing.states.getState(stateStateType.id) : null
|
||||
|
||||
// Extended stateful garagedoor
|
||||
readonly property StateType percentageStateType: thing.thingClass.stateTypes.findByName("percentage")
|
||||
readonly property State percentageState: percentageStateType ? thing.states.getState(percentageStateType.id) : null
|
||||
|
||||
|
||||
// Backward compatiblity with old garagegate interface
|
||||
readonly property StateType intermediatePositionStateType: thing.thingClass.stateTypes.findByName("intermediatePosition")
|
||||
readonly property var intermediatePositionState: intermediatePositionStateType ? device.states.getState(intermediatePositionStateType.id) : null
|
||||
|
||||
// Some garages may also implement the light interface
|
||||
readonly property var lightStateType: thing.thingClass.stateTypes.findByName("power")
|
||||
readonly property var lightState: lightStateType ? thing.states.getState(lightStateType.id) : null
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: app.margins
|
||||
anchors.bottomMargin: app.margins
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: app.largeFont
|
||||
text: garageGateView.thing.name
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: root.landscape ? 2 : 1
|
||||
|
||||
ColorIcon {
|
||||
id: shutterImage
|
||||
Layout.preferredWidth: root.landscape ?
|
||||
Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - app.margins
|
||||
: Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight)
|
||||
Layout.preferredHeight: width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
property string currentImage: {
|
||||
if (garageGateView.isExtended) {
|
||||
return app.pad(Math.round(garageGateView.percentageState.value / 10), 2) + "0"
|
||||
}
|
||||
if (garageGateView.intermediatePositionStateType) {
|
||||
return garageGateView.stateState.value === "closed" ? "100"
|
||||
: garageGateView.intermediatePositionState.value === false ? "000" : "050"
|
||||
}
|
||||
return "100"
|
||||
}
|
||||
name: "../images/garage/garage-" + currentImage + ".svg"
|
||||
|
||||
Item {
|
||||
id: arrows
|
||||
anchors.centerIn: parent
|
||||
width: app.iconSize * 2
|
||||
height: parent.height * .6
|
||||
clip: true
|
||||
visible: garageGateView.stateStateType && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing")
|
||||
property bool up: garageGateView.stateState && garageGateView.stateState.value === "opening"
|
||||
|
||||
// NumberAnimation doesn't reload to/from while it's running. If we switch from closing to opening or vice versa
|
||||
// we need to somehow stop and start the animation
|
||||
property bool animationHack: true
|
||||
onAnimationHackChanged: {
|
||||
if (!animationHack) hackTimer.start();
|
||||
}
|
||||
Timer { id: hackTimer; interval: 1; onTriggered: arrows.animationHack = true }
|
||||
Connections { target: garageGateView.stateState; onValueChanged: arrows.animationHack = false }
|
||||
|
||||
NumberAnimation {
|
||||
target: arrowColumn
|
||||
property: "y"
|
||||
duration: 500
|
||||
easing.type: Easing.Linear
|
||||
from: arrows.up ? app.iconSize : -app.iconSize
|
||||
to: arrows.up ? -app.iconSize : app.iconSize
|
||||
loops: Animation.Infinite
|
||||
running: arrows.animationHack && garageGateView.stateState && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing")
|
||||
}
|
||||
|
||||
Column {
|
||||
id: arrowColumn
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
model: arrows.height / app.iconSize + 1
|
||||
ColorIcon {
|
||||
name: arrows.up ? "../images/up.svg" : "../images/down.svg"
|
||||
width: parent.width
|
||||
height: width
|
||||
color: app.accentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: shutterControlsContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: app.margins * 2
|
||||
Layout.fillHeight: true
|
||||
property int minimumWidth: app.iconSize * 2.5 * (garageGateView.lightState ? 4 : 3)
|
||||
property int minimumHeight: app.iconSize * 2.5
|
||||
|
||||
ItemDelegate {
|
||||
height: app.iconSize * 2
|
||||
width: height
|
||||
anchors.centerIn: parent
|
||||
visible: garageGateView.isImpulseBased
|
||||
ColorIcon {
|
||||
anchors.fill: parent
|
||||
name: "../images/closable-move.svg"
|
||||
anchors.margins: app.margins
|
||||
}
|
||||
onClicked: {
|
||||
var actionTypeId = garageGateView.thing.thingClass.actionTypes.findByName("triggerImpulse").id
|
||||
print("Triggering impulse", actionTypeId)
|
||||
engine.thingManager.executeAction(garageGateView.thing.id, actionTypeId)
|
||||
}
|
||||
}
|
||||
|
||||
ShutterControls {
|
||||
id: shutterControls
|
||||
device: garageGateView.thing
|
||||
anchors.centerIn: parent
|
||||
spacing: (parent.width - app.iconSize*2*children.length) / (children.length - 1)
|
||||
visible: !garageGateView.isImpulseBased
|
||||
|
||||
ItemDelegate {
|
||||
width: app.iconSize * 2
|
||||
height: width
|
||||
visible: garageGateView.lightStateType !== null
|
||||
|
||||
ColorIcon {
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins
|
||||
name: "../images/light-" + (garageGateView.lightState && garageGateView.lightState.value === true ? "on" : "off") + ".svg"
|
||||
color: garageGateView.lightState && garageGateView.lightState.value === true ? Material.accent : keyColor
|
||||
}
|
||||
onClicked: {
|
||||
var params = [];
|
||||
var param = {};
|
||||
param["paramTypeId"] = garageGateView.lightStateType.id;
|
||||
param["value"] = !garageGateView.lightState.value;
|
||||
params.push(param)
|
||||
engine.deviceManager.executeAction(garageGateView.device.id, garageGateView.lightStateType.id, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PageIndicator {
|
||||
anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter }
|
||||
count: garagesFilterModel.count
|
||||
currentIndex: swipeView.currentIndex
|
||||
}
|
||||
}
|
||||
@ -35,12 +35,8 @@ import Nymea 1.0
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import "../components"
|
||||
|
||||
MouseArea {
|
||||
MainViewBase {
|
||||
id: root
|
||||
preventStealing: true
|
||||
onWheel: wheel.accepted = true
|
||||
|
||||
readonly property int count: groupsGridView.count
|
||||
|
||||
GridView {
|
||||
id: groupsGridView
|
||||
@ -491,4 +487,15 @@ MouseArea {
|
||||
device: mediaControllers.count > 0 ? mediaControllers.get(0) : null
|
||||
}
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: groupsGridView.count == 0 && !engine.deviceManager.fetchingData && !engine.tagsManager.busy
|
||||
title: qsTr("There are no groups set up yet.")
|
||||
text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.")
|
||||
imageSource: "../images/view-grid-symbolic.svg"
|
||||
buttonVisible: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -35,15 +35,9 @@ import Nymea 1.0
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import "../components"
|
||||
|
||||
MouseArea {
|
||||
MainViewBase {
|
||||
id: root
|
||||
|
||||
readonly property int count: interfacesGridView.count
|
||||
|
||||
// Prevent scroll events to swipe left/right in case they fall through the grid
|
||||
preventStealing: true
|
||||
onWheel: wheel.accepted = true
|
||||
|
||||
GridView {
|
||||
id: interfacesGridView
|
||||
anchors.fill: parent
|
||||
@ -81,4 +75,37 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: interfacesGridView.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("There are no scenes set up yet.")
|
||||
text: engine.deviceManager.devices.count === 0 ?
|
||||
qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") :
|
||||
qsTr("Scenes provide a useful way to control your things with just one click.")
|
||||
imageSource: "../images/slideshow.svg"
|
||||
buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene")
|
||||
onButtonClicked: {
|
||||
if (engine.deviceManager.devices.count === 0) {
|
||||
pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
|
||||
} else {
|
||||
var newRule = engine.ruleManager.createNewRule();
|
||||
d.editRulePage = pageStack.push(Qt.resolvedUrl("../magic/EditRulePage.qml"), {rule: newRule });
|
||||
d.editRulePage.startAddAction();
|
||||
d.editRulePage.StackView.onRemoved.connect(function() {
|
||||
newRule.destroy();
|
||||
})
|
||||
d.editRulePage.onAccept.connect(function() {
|
||||
d.editRulePage.busy = true;
|
||||
engine.ruleManager.addRule(d.editRulePage.rule);
|
||||
})
|
||||
d.editRulePage.onCancel.connect(function() {
|
||||
pageStack.pop();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
83
nymea-app/ui/mainviews/ThingsView.qml
Normal file
83
nymea-app/ui/mainviews/ThingsView.qml
Normal file
@ -0,0 +1,83 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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.8
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
import "../delegates"
|
||||
|
||||
MainViewBase {
|
||||
id: root
|
||||
|
||||
InterfacesSortModel {
|
||||
id: mainModel
|
||||
interfacesModel: InterfacesModel {
|
||||
engine: _engine
|
||||
devices: DevicesProxy {
|
||||
engine: _engine
|
||||
}
|
||||
shownInterfaces: app.supportedInterfaces
|
||||
showUncategorized: true
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: interfacesGridView
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins / 2
|
||||
model: mainModel
|
||||
|
||||
readonly property int minTileWidth: 172
|
||||
readonly property int tilesPerRow: root.width / minTileWidth
|
||||
|
||||
cellWidth: width / tilesPerRow
|
||||
cellHeight: cellWidth
|
||||
delegate: InterfaceTile {
|
||||
width: interfacesGridView.cellWidth
|
||||
height: interfacesGridView.cellHeight
|
||||
iface: Interfaces.findByName(model.name)
|
||||
}
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins }
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData
|
||||
title: qsTr("Welcome to %1!").arg(app.systemName)
|
||||
// Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice
|
||||
text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName)
|
||||
imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
|
||||
buttonText: qsTr("Add a thing")
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
|
||||
}
|
||||
}
|
||||
@ -309,7 +309,7 @@ Page {
|
||||
}
|
||||
BusyIndicator {
|
||||
running: visible
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user