diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 54c627011..e14e57848 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -2,6 +2,7 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.0 import QtQuick.Controls.Material 2.0 +import QtQml.Models 2.2 import org.electrum 1.0 @@ -15,99 +16,149 @@ Pane { width: parent.width height: parent.height - model: Daemon.currentWallet.historyModel + model: visualModel - delegate: Item { - id: delegate + section.property: 'section' + section.criteria: ViewSection.FullString + section.delegate: RowLayout { width: ListView.view.width - height: delegateLayout.height - - ColumnLayout { - id: delegateLayout - width: parent.width - spacing: 0 - - Rectangle { - visible: index > 0 - Layout.fillWidth: true - Layout.preferredHeight: constants.paddingSmall - color: Qt.rgba(0,0,0,0.10) - } - + required property string section + Label { + text: section == 'today' + ? qsTr('Today') + : section == 'yesterday' + ? qsTr('Yesterday') + : section == 'lastweek' + ? qsTr('Last week') + : section == 'lastmonth' + ? qsTr('Last month') + : qsTr('Older') + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingLarge + font.pixelSize: constants.fontSizeLarge + color: constants.mutedForeground + } + } - ItemDelegate { - Layout.fillWidth: true - Layout.preferredHeight: txinfo.height - - GridLayout { - id: txinfo - columns: 3 - - x: constants.paddingSmall - width: delegate.width - 2*constants.paddingSmall - - Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1} - Image { - readonly property variant tx_icons : [ - "../../../gui/icons/unconfirmed.png", - "../../../gui/icons/clock1.png", - "../../../gui/icons/clock2.png", - "../../../gui/icons/clock3.png", - "../../../gui/icons/clock4.png", - "../../../gui/icons/clock5.png", - "../../../gui/icons/confirmed.png" - ] - - Layout.preferredWidth: constants.iconSizeLarge - Layout.preferredHeight: constants.iconSizeLarge - Layout.alignment: Qt.AlignVCenter - Layout.rowSpan: 2 - source: tx_icons[Math.min(6,model.confirmations)] + DelegateModel { + id: visualModel + model: Daemon.currentWallet.historyModel + + groups: [ + DelegateModelGroup { name: 'today'; includeByDefault: false }, + DelegateModelGroup { name: 'yesterday'; includeByDefault: false }, + DelegateModelGroup { name: 'lastweek'; includeByDefault: false }, + DelegateModelGroup { name: 'lastmonth'; includeByDefault: false }, + DelegateModelGroup { name: 'older'; includeByDefault: false } + ] + + delegate: Item { + id: delegate + width: ListView.view.width + height: delegateLayout.height + + ColumnLayout { + id: delegateLayout + width: parent.width + spacing: 0 + + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: txinfo.height + + GridLayout { + id: txinfo + columns: 3 + + x: constants.paddingSmall + width: delegate.width - 2*constants.paddingSmall + + Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1} + Image { + readonly property variant tx_icons : [ + "../../../gui/icons/unconfirmed.png", + "../../../gui/icons/clock1.png", + "../../../gui/icons/clock2.png", + "../../../gui/icons/clock3.png", + "../../../gui/icons/clock4.png", + "../../../gui/icons/clock5.png", + "../../../gui/icons/confirmed.png" + ] + + Layout.preferredWidth: constants.iconSizeLarge + Layout.preferredHeight: constants.iconSizeLarge + Layout.alignment: Qt.AlignVCenter + Layout.rowSpan: 2 + source: tx_icons[Math.min(6,model.confirmations)] + } + + Label { + font.pixelSize: constants.fontSizeLarge + Layout.fillWidth: true + text: model.label !== '' ? model.label : '' + color: model.label !== '' ? Material.accentColor : 'gray' + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + } + Label { + id: valueLabel + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + Layout.alignment: Qt.AlignRight + text: Config.formatSats(model.bc_value) + font.bold: true + color: model.incoming ? constants.colorCredit : constants.colorDebit + } + Label { + font.pixelSize: constants.fontSizeSmall + text: model.date + } + Label { + font.pixelSize: constants.fontSizeXSmall + Layout.alignment: Qt.AlignRight + text: model.fee !== undefined ? 'fee: ' + model.fee : '' + } + Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1 } } + } - Label { - font.pixelSize: constants.fontSizeLarge - Layout.fillWidth: true - text: model.label !== '' ? model.label : '' - color: model.label !== '' ? Material.accentColor : 'gray' - wrapMode: Text.Wrap - maximumLineCount: 2 - elide: Text.ElideRight - } - Label { - id: valueLabel - font.family: FixedFont - font.pixelSize: constants.fontSizeMedium - text: Config.formatSats(model.bc_value) - font.bold: true - color: model.incoming ? constants.colorCredit : constants.colorDebit - } - Label { - font.pixelSize: constants.fontSizeSmall - text: model.date - } - Label { - font.pixelSize: constants.fontSizeXSmall - Layout.alignment: Qt.AlignRight - text: model.fee !== undefined ? 'fee: ' + model.fee : '' - } - Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1 } + Rectangle { + visible: delegate.ListView.section == delegate.ListView.nextSection + Layout.fillWidth: true + Layout.preferredHeight: constants.paddingTiny + color: Qt.rgba(0,0,0,0.10) } + } - } - // as the items in the model are not bindings to QObjects, - // hook up events that might change the appearance - Connections { - target: Config - function onBaseUnitChanged() { - valueLabel.text = Config.formatSats(model.bc_value) + // as the items in the model are not bindings to QObjects, + // hook up events that might change the appearance + Connections { + target: Config + function onBaseUnitChanged() { + valueLabel.text = Config.formatSats(model.bc_value) + } + function onThousandsSeparatorChanged() { + valueLabel.text = Config.formatSats(model.bc_value) + } } - function onThousandsSeparatorChanged() { - valueLabel.text = Config.formatSats(model.bc_value) + + Component.onCompleted: { + if (model.section == 'today') { + delegate.DelegateModel.inToday = true + } else if (model.section == 'yesterday') { + delegate.DelegateModel.inYesterday = true + } else if (model.section == 'lastweek') { + delegate.DelegateModel.inLastweek = true + } else if (model.section == 'lastmonth') { + delegate.DelegateModel.inLastmonth = true + } else if (model.section == 'older') { + delegate.DelegateModel.inOlder = true + } } - } - } // delegate + } // delegate + } ScrollIndicator.vertical: ScrollIndicator { } diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 224824955..0e05b40c8 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex @@ -17,7 +17,7 @@ class QETransactionListModel(QAbstractListModel): # define listmodel rolemap _ROLE_NAMES=('txid','fee_sat','height','confirmations','timestamp','monotonic_timestamp', 'incoming','bc_value','bc_balance','date','label','txpos_in_block','fee', - 'inputs','outputs') + 'inputs','outputs','section') _ROLE_KEYS = range(Qt.UserRole + 1, Qt.UserRole + 1 + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) _ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) @@ -43,14 +43,38 @@ class QETransactionListModel(QAbstractListModel): self.tx_history = [] self.endResetModel() + def tx_to_model(self, tx): + item = tx + for output in item['outputs']: + output['value'] = output['value'].value + + # newly arriving txs have no (block) timestamp + # TODO? + if not item['timestamp']: + item['timestamp'] = datetime.timestamp(datetime.now()) + + txts = datetime.fromtimestamp(item['timestamp']) + today = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) + + if (txts > today): + item['section'] = 'today' + elif (txts > today - timedelta(days=1)): + item['section'] = 'yesterday' + elif (txts > today - timedelta(days=7)): + item['section'] = 'lastweek' + elif (txts > today - timedelta(days=31)): + item['section'] = 'lastmonth' + else: + item['section'] = 'older' + + return item + # initial model data def init_model(self): history = self.wallet.get_detailed_history(show_addresses = True) - txs = history['transactions'] - # use primitives - for tx in txs: - for output in tx['outputs']: - output['value'] = output['value'].value + txs = [] + for tx in history['transactions']: + txs.append(self.tx_to_model(tx)) self.clear() self.beginInsertRows(QModelIndex(), 0, len(txs) - 1)