// Copyright (c) 2014-2019, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.2 import moneroComponents.Wallet 1.0 import moneroComponents.WalletManager 1.0 import moneroComponents.TransactionHistory 1.0 import moneroComponents.TransactionInfo 1.0 import moneroComponents.TransactionHistoryModel 1.0 import moneroComponents.Clipboard 1.0 import FontAwesome 1.0 import "../components" as MoneroComponents import "../js/Utils.js" as Utils import "../js/TxUtils.js" as TxUtils Rectangle { id: root property var model property int sideMargin: 50 * scaleRatio property var initialized: false property int txMax: 5 property int txOffset: 0 property int txPage: (txOffset / 5) + 1 property int txCount: 0 property var sortSearchString: null property bool sortDirection: true // true = desc, false = asc property string sortBy: "blockheight" property var txModelData: [] // representation of transaction data (appWindow.currentWallet.historyModel) property var txData: [] // representation of FILTERED transation data property var txDataCollapsed: [] // keep track of which txs are collapsed property string historyStatusMessage: "" property alias contentHeight: pageRoot.height Clipboard { id: clipboard } ListModel { id: txListViewModel } color: "transparent" ColumnLayout { id: pageRoot anchors.topMargin: 40 * scaleRatio anchors.left: parent.left anchors.top: parent.top anchors.right: parent.right RowLayout { Layout.preferredHeight: 24 Layout.preferredWidth: parent.width - root.sideMargin Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin Layout.bottomMargin: 10 * scaleRatio MoneroComponents.Label { fontSize: 24 * scaleRatio text: qsTr("Transactions") + translationManager.emptyString } Item { Layout.fillWidth: true } RowLayout { id: sortAndFilter property bool collapsed: false Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.preferredWidth: 100 Layout.preferredHeight: 15 spacing: 8 * scaleRatio Text { Layout.alignment: Qt.AlignVCenter font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Sort & filter") + translationManager.emptyString color: MoneroComponents.Style.defaultFontColor MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { sortAndFilter.collapsed = !sortAndFilter.collapsed } } } Image { Layout.alignment: Qt.AlignVCenter height: 8 * scaleRatio width: 12 * scaleRatio source: "../images/whiteDropIndicator.png" rotation: parent.collapsed ? 0 : 180 MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { sortAndFilter.collapsed = !sortAndFilter.collapsed } } } } } ColumnLayout { Layout.fillWidth: true Layout.topMargin: 8 * scaleRatio Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin visible: sortAndFilter.collapsed MoneroComponents.LineEdit { id: searchInput Layout.fillWidth: true input.topPadding: 6 * scaleRatio input.bottomPadding: 6 * scaleRatio fontSize: 16 * scaleRatio labelFontSize: 14 * scaleRatio placeholderText: qsTr("Search...") + translationManager.emptyString placeholderFontSize: 16 * scaleRatio inputHeight: 34 onTextUpdated: { if(searchInput.text != null && searchInput.text.length >= 3){ root.sortSearchString = searchInput.text; root.reset(); root.updateFilter(); } else { root.sortSearchString = null; root.reset(); root.updateFilter(); } } } } GridLayout { visible: sortAndFilter.collapsed Layout.fillWidth: true Layout.topMargin: 4 * scaleRatio Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin columns: 2 columnSpacing: 20 * scaleRatio z: 6 MoneroComponents.DatePicker { id: fromDatePicker Layout.fillWidth: true width: 100 * scaleRatio inputLabel.text: qsTr("Date from") + translationManager.emptyString inputLabel.font.pixelSize: 14 * scaleRatio onCurrentDateChanged: { if(root.initialized){ root.reset(); root.updateFilter(); } } } MoneroComponents.DatePicker { id: toDatePicker Layout.fillWidth: true width: 100 * scaleRatio inputLabel.text: qsTr("Date to") + translationManager.emptyString onCurrentDateChanged: { if(root.initialized){ root.reset(); root.updateFilter(); } } } } RowLayout { Layout.topMargin: 20 * scaleRatio Layout.bottomMargin: 20 * scaleRatio Layout.fillWidth: true Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin Rectangle { visible: sortAndFilter.collapsed color: "transparent" Layout.preferredWidth: childrenRect.width + 38 * scaleRatio Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Sort by") + ":" color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { visible: sortAndFilter.collapsed id: sortBlockheight color: "transparent" Layout.preferredWidth: sortBlockheightText.width + 42 * scaleRatio Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent Text { id: sortBlockheightText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Blockheight") + translationManager.emptyString color: root.sortBy === "blockheight" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor anchors.verticalCenter: parent.verticalCenter } Image { height: 8 * scaleRatio width: 12 * scaleRatio visible: root.sortBy === "blockheight" ? true : false opacity: root.sortBy === "blockheight" ? 1 : 0.6 source: "../images/whiteDropIndicator.png" rotation: { if(root.sortBy === "blockheight"){ return root.sortDirection ? 0 : 180 } else { return 0; } } } Item { Layout.fillWidth: true } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { if(root.sortBy !== "blockheight") { root.sortDirection = true; } else { root.sortDirection = !root.sortDirection } root.sortBy = "blockheight"; root.updateSort(); } } } Rectangle { visible: sortAndFilter.collapsed color: "transparent" Layout.preferredWidth: sortDateText.width + 42 * scaleRatio Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent Text { id: sortDateText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Date") + translationManager.emptyString color: root.sortBy === "timestamp" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor anchors.verticalCenter: parent.verticalCenter } Image { height: 8 * scaleRatio width: 12 * scaleRatio visible: root.sortBy === "timestamp" ? true : false opacity: root.sortBy === "timestamp" ? 1 : 0.6 source: "../images/whiteDropIndicator.png" rotation: { if(root.sortBy === "timestamp"){ return root.sortDirection ? 0 : 180 } else { return 0; } } } Item { Layout.fillWidth: true } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { if(root.sortBy !== "timestamp") { root.sortDirection = true; } else { root.sortDirection = !root.sortDirection } root.sortBy = "timestamp"; root.updateSort(); } } } Rectangle { visible: sortAndFilter.collapsed color: "transparent" Layout.preferredWidth: sortAmountText.width + 42 * scaleRatio Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent Text { id: sortAmountText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Amount") + translationManager.emptyString color: root.sortBy === "amount" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor anchors.verticalCenter: parent.verticalCenter } Image { height: 8 * scaleRatio width: 12 * scaleRatio visible: root.sortBy === "amount" ? true : false opacity: root.sortBy === "amount" ? 1 : 0.6 source: "../images/whiteDropIndicator.png" rotation: { if(root.sortBy === "amount"){ return root.sortDirection ? 0 : 180 } else { return 0; } } } Item { Layout.fillWidth: true } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { if(root.sortBy !== "amount") { root.sortDirection = true; } else { root.sortDirection = !root.sortDirection } root.sortBy = "amount"; root.updateSort(); } } } Rectangle { visible: !sortAndFilter.collapsed Layout.preferredHeight: 20 Text { // status message font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: root.historyStatusMessage color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Item { Layout.fillWidth: true } RowLayout { id: pagination spacing: 0 Layout.alignment: Qt.AlignRight Layout.preferredWidth: childrenRect.width Layout.preferredHeight: 20 Rectangle { color: "transparent" Layout.preferredWidth: childrenRect.width + 2 * scaleRatio Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Page") + ":" color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.preferredWidth: childrenRect.width + 10 * scaleRatio Layout.leftMargin: 4 * scaleRatio Layout.preferredHeight: 20 * scaleRatio Text { id: paginationText text: root.txPage + "/" + Math.ceil(root.txCount / root.txMax) color: "white" anchors.verticalCenter: parent.verticalCenter MouseArea { // jump to page functionality property int pages: Math.ceil(root.txCount / root.txMax) anchors.fill: parent hoverEnabled: pages > 1 cursorShape: hoverEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor onClicked: { if(pages === 1) return; inputDialog.labelText = qsTr("Jump to page (1-%1)").arg(pages) + translationManager.emptyString; inputDialog.inputText = "1"; inputDialog.onAcceptedCallback = function() { var pageNumber = parseInt(inputDialog.inputText); if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= pages) { root.paginationJump(parseInt(pageNumber)); return; } appWindow.showStatusMessage(qsTr("Invalid page. Must be a number within the specified range."), 4); } inputDialog.onRejectedCallback = null; inputDialog.open() } } } } Rectangle { id: paginationPrev Layout.preferredWidth: 18 * scaleRatio Layout.preferredHeight: 20 * scaleRatio color: "transparent" opacity: enabled ? 1.0 : 0.6 enabled: false Image { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left height: 8 * scaleRatio width: 12 * scaleRatio source: "../images/whiteDropIndicator.png" rotation: 90 } MouseArea { enabled: parent.enabled anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root.paginationPrevClicked(); } } } Rectangle { id: paginationNext Layout.preferredWidth: 18 * scaleRatio Layout.preferredHeight: 20 * scaleRatio color: "transparent" opacity: enabled ? 1.0 : 0.6 enabled: false Image { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right height: 8 * scaleRatio width: 12 * scaleRatio source: "../images/whiteDropIndicator.png" rotation: 270 } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root.paginationNextClicked(); } } } } } ListView { visible: true id: txListview Layout.fillWidth: true Layout.preferredHeight: contentHeight; model: txListViewModel interactive: false delegate: Rectangle { id: delegate property bool collapsed: root.txDataCollapsed.indexOf(hash) >= 0 ? true : false anchors.left: parent.left anchors.right: parent.right height: { if(!collapsed) return 60; if(isout && delegate.address !== "") return 320; return 220; } color: collapsed ? "#06FFFFFF" : "transparent" Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left width: sideMargin color: "transparent" Rectangle { anchors.top: parent.top anchors.topMargin: 24 * scaleRatio anchors.horizontalCenter: parent.horizontalCenter width: 10 * scaleRatio height: 10 * scaleRatio radius: 8 * scaleRatio color: isout ? "#d85a00" : "#2eb358" } } ColumnLayout { spacing: 0 clip: true height: parent.height anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: sideMargin anchors.rightMargin: sideMargin RowLayout { spacing: 0 Layout.fillWidth: true height: 60 Layout.preferredHeight: 60 ColumnLayout { spacing: 0 clip: true Layout.preferredHeight: 120 Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: (isout ? qsTr("Sent") : qsTr("Received")) + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: _amount + " XMR" color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: (isout ? qsTr("Fee") : confirmationsRequired === 60 ? qsTr("Mined") : qsTr("Fee")) + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: { if(!isout && confirmationsRequired === 60) return qsTr("Yes") + translationManager.emptyString; if(fee !== "") return fee + " XMR"; return "-"; } color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } } ColumnLayout { spacing: 0 clip: true Layout.preferredHeight: 120 Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Blockheight") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 14 * scaleRatio text: blockheight > 0 ? blockheight : qsTr('Pending') + translationManager.emptyString; color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Confirmations") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { property bool confirmed: confirmations < confirmationsRequired ? false : true font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: confirmed ? confirmations : confirmations + "/" + confirmationsRequired color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } } ColumnLayout { spacing: 0 clip: true Layout.preferredHeight: 120 Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Date") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: persistentSettings.historyHumanDates ? dateHuman : date + " " + time color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Item { Layout.fillWidth: true Layout.preferredHeight: 60 MoneroComponents.StandardButton { id: btnDetails text: FontAwesome.info small: true label.font.family: FontAwesome.fontFamily fontSize: 18 * scaleRatio width: 28 * scaleRatio MouseArea { state: "details" anchors.fill: parent hoverEnabled: true z: parent.z + 1 } } Image { visible: !isout && confirmationsRequired === 60 anchors.left: btnDetails.right anchors.leftMargin: 16 * scaleRatio width: 28 height: 28 source: "qrc:///images/miningxmr.png" } MoneroComponents.StandardButton { visible: isout anchors.left: btnDetails.right anchors.leftMargin: 10 * scaleRatio text: FontAwesome.productHunt small: true label.font.family: FontAwesome.fontFamily fontSize: 18 * scaleRatio width: 36 * scaleRatio MouseArea { state: "proof" anchors.fill: parent hoverEnabled: true z: parent.z + 1 } } } } } ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.preferredHeight: 40 Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Description") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { id: txNoteText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: tx_note !== "" ? tx_note : "-" color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } Image { anchors.top: parent.top anchors.left: txNoteText.right anchors.leftMargin: 12 * scaleRatio source: "../images/editIcon.png" opacity: 1 width: 18 height: 18 MouseArea { id: txNoteArea state: "set_tx_note" anchors.fill: parent hoverEnabled: true onEntered: { parent.opacity = 0.7; } onExited: { parent.opacity = 1.0; } cursorShape: Qt.PointingHandCursor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Transaction ID") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: hash color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Transaction key") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: { var txKey = currentWallet.getTxKey(hash) if(txKey) return txKey; else return "-" } color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { visible: isout color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: qsTr("Address sent to") + translationManager.emptyString color: "#C0C0C0" anchors.verticalCenter: parent.verticalCenter } } Rectangle { visible: isout color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 Text { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: { if(isout && address !== ""){ return TxUtils.addressTruncate(address, 24); } if(isout && blockheight === 0) return qsTr("Waiting for transaction to leave txpool.") + translationManager.emptyString else return qsTr("Unknown recipient") + translationManager.emptyString; } color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable_address" anchors.fill: parent hoverEnabled: true onEntered: parent.color = MoneroComponents.Style.orange onExited: parent.color = MoneroComponents.Style.defaultFontColor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } } Item { Layout.fillWidth: true Layout.fillHeight: true } } MouseArea { id: collapseArea objectName: "collapseArea" cursorShape: Qt.PointingHandCursor anchors.fill: parent onClicked: { // detect clicks on text (for copying), otherwise toggle collapse var doCollapse = true; var res = Utils.qmlEach(delegate, ['containsMouse', 'preventStealing', 'scrollGestureEnabled'], ['collapseArea'], []); for(var i = 0; i < res.length; i+=1){ if(res[i].containsMouse === true){ if(res[i].state === 'copyable' && res[i].parent.hasOwnProperty('text')) toClipboard(res[i].parent.text); if(res[i].state === 'copyable_address') root.toClipboard(address); if(res[i].state === 'set_tx_note') root.editDescription(hash); if(res[i].state === 'details') root.showTxDetails(hash, paymentId, destinations, subaddrAccount, subaddrIndex); if(res[i].state === 'proof') root.showTxProof(hash, paymentId, destinations, subaddrAccount, subaddrIndex); doCollapse = false; break; } } if(doCollapse){ collapsed = !collapsed; // remember collapsed state if(collapsed){ root.txDataCollapsed.push(hash); } else { root.removeFromCollapsedList(hash); } } } } Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right width: sideMargin color: "transparent" Image { anchors.top: parent.top anchors.topMargin: 24 * scaleRatio anchors.horizontalCenter: parent.horizontalCenter height: 8 * scaleRatio width: 12 * scaleRatio source: "../images/whiteDropIndicator.png" rotation: delegate.collapsed ? 180 : 0 } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 1 color: "#2b2b2b" } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.bottom height: 1 color: "#2b2b2b" } } } Item { visible: sortAndFilter.collapsed Layout.topMargin: 10 * scaleRatio Layout.bottomMargin: 10 * scaleRatio Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin Text { // status message Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 * scaleRatio text: root.historyStatusMessage; color: MoneroComponents.Style.dimmedFontColor } } MoneroComponents.CheckBox2 { id: showAdvancedCheckbox Layout.topMargin: 30 * scaleRatio Layout.bottomMargin: 20 * scaleRatio Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin checked: persistentSettings.historyShowAdvanced onClicked: persistentSettings.historyShowAdvanced = !persistentSettings.historyShowAdvanced text: qsTr("Advanced options") + translationManager.emptyString } ColumnLayout { visible: persistentSettings.historyShowAdvanced Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin spacing: 20 * scaleRatio MoneroComponents.CheckBox { id: humanDatesCheckBox checked: persistentSettings.historyHumanDates onClicked: { persistentSettings.historyHumanDates = !persistentSettings.historyHumanDates root.updateDisplay(root.txOffset, root.txMax, false); } text: qsTr("Human readable date format") + translationManager.emptyString } MoneroComponents.StandardButton { visible: !isIOS small: true text: qsTr("Export all history") + translationManager.emptyString onClicked: { writeCSVFileDialog.open(); } } } } function refresh(){ if(appWindow.currentWallet != null && typeof appWindow.currentWallet.history !== "undefined" ) { currentWallet.history.refresh(currentWallet.currentSubaddressAccount); } if (typeof root.model !== 'undefined' && root.model != null) { toDatePicker.currentDate = root.model.transactionHistory.lastDateTime } // extract from model, create JS array of txs root.updateTransactionsFromModel(); // fill listview, update UI root.updateDisplay(root.txOffset, root.txMax); } function reset() { root.txOffset = 0; root.txMax = 5; if (typeof root.model !== 'undefined' && root.model != null) { root.model.dateFromFilter = "2014-04-18" // genesis block root.model.dateToFilter = "9999-09-09" // fix before september 9999 // negative values disable filters here; root.model.amountFromFilter = -1; root.model.amountToFilter = -1; root.model.directionFilter = TransactionInfo.Direction_Both; } } function updateFilter(){ // applying filters root.txData = JSON.parse(JSON.stringify(root.txModelData)); // deepcopy var fromDate = fromDatePicker.currentDate.getTime() / 1000; var toDate = toDatePicker.currentDate.getTime() / 1000; var txs = []; for (var i = 0; i < root.txData.length; i++){ var item = root.txData[i]; var matched = ""; // daterange filtering if(item.timestamp < fromDate || item.timestamp > toDate){ continue; } // search string filtering if(root.sortSearchString == null || root.sortSearchString === ""){ txs.push(root.txData[i]); continue; } if(root.sortSearchString.length >= 1){ if(item.amount && item.amount.toString().startsWith(root.sortSearchString)){ txs.push(item); } else if(item.address !== "" && item.address.startsWith(root.sortSearchString)){ txs.push(item); } else if(item.blockheight.toString().startsWith(root.sortSearchString)) { txs.push(item); } else if (item.hash.startsWith(root.sortSearchString)){ txs.push(item); } } } root.txData = txs; root.txCount = root.txData.length; root.updateSort(); root.updateDisplay(root.txOffset, root.txMax); } function updateSort(){ // applying sorts root.txOffset = 0; root.txData.sort(function(a, b) { return a[root.sortBy] - b[root.sortBy]; }); if(root.sortDirection) root.txData.reverse(); root.updateDisplay(root.txOffset, root.txMax); } function updateDisplay(tx_offset, tx_max, auto_collapse) { if(typeof auto_collapse === 'undefined') auto_collapse = false; txListViewModel.clear(); // limit results as per tx_max (root.txMax) var txs = root.txData.slice(tx_offset, tx_offset + tx_max); // make first result on the first page collapsed by default if(auto_collapse && root.txPage === 1 && txs.length > 0 && (root.sortSearchString == null || root.sortSearchString === "")) root.txDataCollapsed.push(txs[0]['hash']); // populate listview for (var i = 0; i < txs.length; i++){ txListViewModel.append(txs[i]); } root.updateHistoryStatusMessage(); // determine pagination button states var count = txData.length; if(count <= root.txMax) { paginationPrev.enabled = false; paginationNext.enabled = false; return; } if(root.txOffset < root.txMax) paginationPrev.enabled = false; else paginationPrev.enabled = true; if((root.txOffset + root.txMax) >= count) paginationNext.enabled = false; else paginationNext.enabled = true; } function updateTransactionsFromModel() { // This function copies the items of `appWindow.currentWallet.historyModel` to `root.txModelData`, as a list of javascript objects if(currentWallet == null || typeof currentWallet.history === "undefined" ) return; var _model = root.model; var total = 0 var count = _model.rowCount() root.txModelData = []; for (var i = 0; i < count; ++i) { var idx = _model.index(i, 0); var isout = _model.data(idx, TransactionHistoryModel.TransactionIsOutRole); var amount = _model.data(idx, TransactionHistoryModel.TransactionAmountRole); var hash = _model.data(idx, TransactionHistoryModel.TransactionHashRole); var paymentId = _model.data(idx, TransactionHistoryModel.TransactionPaymentIdRole); var destinations = _model.data(idx, TransactionHistoryModel.TransactionDestinationsRole); var time = _model.data(idx, TransactionHistoryModel.TransactionTimeRole); var date = _model.data(idx, TransactionHistoryModel.TransactionDateRole); var blockheight = _model.data(idx, TransactionHistoryModel.TransactionBlockHeightRole); var confirmations = _model.data(idx, TransactionHistoryModel.TransactionConfirmationsRole); var confirmationsRequired = _model.data(idx, TransactionHistoryModel.TransactionConfirmationsRequiredRole); var fee = _model.data(idx, TransactionHistoryModel.TransactionFeeRole); var subaddrAccount = model.data(idx, TransactionHistoryModel.TransactionSubaddrAccountRole); var subaddrIndex = model.data(idx, TransactionHistoryModel.TransactionSubaddrIndexRole); var timestamp = new Date(date + " " + time).getTime() / 1000; var dateHuman = Utils.ago(timestamp); var _amount = amount; if(_amount === 0){ // *sometimes* amount is 0, while the 'destinations string' // has the correct amount, so we try to fetch it from that instead. _amount = TxUtils.destinationsToAmount(destinations); _amount = Number(_amount *1); } var tx_note = currentWallet.getUserNote(hash); var address = ""; if(isout) { address = TxUtils.destinationsToAddress(destinations); } if (isout) total = walletManager.subi(total, amount) else total = walletManager.addi(total, amount) root.txModelData.push({ "i": i, "isout": isout, "amount": Number(amount), "_amount": _amount, "hash": hash, "paymentId": paymentId, "address": address, "destinations": destinations, "tx_note": tx_note, "time": time, "date": date, "dateHuman": dateHuman, "blockheight": blockheight, "address": address, "timestamp": timestamp, "fee": fee, "confirmations": confirmations, "confirmationsRequired": confirmationsRequired, "subaddrAccount": subaddrAccount, "subaddrIndex": subaddrIndex }); } root.txData = JSON.parse(JSON.stringify(root.txModelData)); // deepcopy root.txCount = root.txData.length; } function update() { // handle outside mutation of tx model; incoming/outgoing funds or new blocks. Update table. currentWallet.history.refresh(currentWallet.currentSubaddressAccount); root.updateTransactionsFromModel(); root.updateFilter(); } function editDescription(_hash){ inputDialog.labelText = qsTr("Set description:") + translationManager.emptyString; inputDialog.onAcceptedCallback = function() { appWindow.currentWallet.setUserNote(_hash, inputDialog.inputText); appWindow.showStatusMessage(qsTr("Updated description."),3); root.update(); } inputDialog.onRejectedCallback = null; inputDialog.open(); } function paginationPrevClicked(){ root.txOffset -= root.txMax; updateDisplay(root.txOffset, root.txMax); } function paginationNextClicked(){ root.txOffset += root.txMax; updateDisplay(root.txOffset, root.txMax); } function paginationJump(pageNumber){ root.txOffset = root.txMax * Math.ceil(pageNumber - 1 || 0); updateDisplay(root.txOffset, root.txMax); } function removeFromCollapsedList(hash){ root.txDataCollapsed = root.txDataCollapsed.filter(function(item) { return item !== hash }); } function updateHistoryStatusMessage(){ if(root.txModelData.length <= 0){ root.historyStatusMessage = qsTr("No transaction history yet.") + translationManager.emptyString; } else if (root.txData.length <= 0){ root.historyStatusMessage = qsTr("No results.") + translationManager.emptyString; } else { root.historyStatusMessage = qsTr("%1 transactions total, showing %2.").arg(root.txData.length).arg(txListViewModel.count) + translationManager.emptyString; } } function showTxDetails(hash, paymentId, destinations, subaddrAccount, subaddrIndex){ var tx_key = currentWallet.getTxKey(hash) var tx_note = currentWallet.getUserNote(hash) var rings = currentWallet.getRings(hash) var address_label = subaddrIndex == 0 ? (qsTr("Primary address") + translationManager.emptyString) : currentWallet.getSubaddressLabel(subaddrAccount, subaddrIndex) var address = currentWallet.address(subaddrAccount, subaddrIndex) if (rings) rings = rings.replace(/\|/g, '\n') informationPopup.title = qsTr("Transaction details") + translationManager.emptyString; informationPopup.content = buildTxDetailsString(hash, paymentId, tx_key, tx_note, destinations, rings, address, address_label); informationPopup.onCloseCallback = null informationPopup.open(); } function showTxProof(hash, paymentId, destinations, subaddrAccount, subaddrIndex){ var address = TxUtils.destinationsToAddress(destinations); if(address === undefined){ console.log('getProof: Error fetching address') return; } var checked = (TxUtils.checkTxID(hash) && TxUtils.checkAddress(address, appWindow.persistentSettings.nettype)); if(!checked){ console.log('getProof: Error checking TxId and/or address'); } console.log("getProof: Generate clicked: txid " + hash + ", address " + address); middlePanel.getProofClicked(hash, address, ''); } function toClipboard(text){ console.log("Copied to clipboard"); clipboard.setText(text); appWindow.showStatusMessage(qsTr("Copied to clipboard"),3); } function buildTxDetailsString(tx_id, paymentId, tx_key,tx_note, destinations, rings, address, address_label) { var trStart = '', trMiddle = '', trEnd = ""; return '' + (tx_id ? trStart + qsTr("Tx ID:") + trMiddle + tx_id + trEnd : "") + (address_label ? trStart + qsTr("Address label:") + trMiddle + address_label + trEnd : "") + (address ? trStart + qsTr("Address:") + trMiddle + address + trEnd : "") + (paymentId ? trStart + qsTr("Payment ID:") + trMiddle + paymentId + trEnd : "") + (tx_key ? trStart + qsTr("Tx key:") + trMiddle + tx_key + trEnd : "") + (tx_note ? trStart + qsTr("Tx note:") + trMiddle + tx_note + trEnd : "") + (destinations ? trStart + qsTr("Destinations:") + trMiddle + destinations + trEnd : "") + (rings ? trStart + qsTr("Rings:") + trMiddle + rings + trEnd : "") + "
" + translationManager.emptyString; } function lookupPaymentID(paymentId) { if (!addressBookModel) return "" var idx = addressBookModel.lookupPaymentID(paymentId) if (idx < 0) return "" idx = addressBookModel.index(idx, 0) return addressBookModel.data(idx, AddressBookModel.AddressBookDescriptionRole) } FileDialog { id: writeCSVFileDialog title: qsTr("Please choose a folder") + translationManager.emptyString selectFolder: true onRejected: { console.log("csv write canceled") } onAccepted: { var dataDir = walletManager.urlToLocalPath(writeCSVFileDialog.fileUrl); var written = currentWallet.history.writeCSV(currentWallet.currentSubaddressAccount, dataDir); if(written !== ""){ informationPopup.title = qsTr("Success") + translationManager.emptyString; var text = qsTr("CSV file written to: %1").arg(written) + "\n\n" text += qsTr("Tip: Use your favorite spreadsheet software to sort on blockheight.") + "\n\n" + translationManager.emptyString; informationPopup.text = text; informationPopup.icon = StandardIcon.Information; } else { informationPopup.title = qsTr("Error") + translationManager.emptyString; informationPopup.text = qsTr("Error exporting transaction data.") + "\n\n" + translationManager.emptyString; informationPopup.icon = StandardIcon.Critical; } informationPopup.onCloseCallback = null; informationPopup.open(); } Component.onCompleted: { var _folder = 'file://' + moneroAccountsDir; try { _folder = 'file://' + desktopFolder; } catch(err) {} finally { writeCSVFileDialog.folder = _folder; } } } function onPageCompleted() { // setup date filter scope according to real transactions if(appWindow.currentWallet != null){ root.model = appWindow.currentWallet.historyModel; root.model.sortRole = TransactionHistoryModel.TransactionBlockHeightRole root.model.sort(0, Qt.DescendingOrder); fromDatePicker.currentDate = model.transactionHistory.firstDateTime } root.reset(); root.refresh(); root.initialized = true; } function onPageClosed(){ root.initialized = false; root.reset(); } }