// 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 QtGraphicalEffects 1.0 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/effects/" as MoneroEffects 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 property var initialized: false property int txMax: Math.max(5, ((appWindow.height - 250) / 60)) property int txOffset: 0 property int txPage: (txOffset / txMax) + 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" onTxMaxChanged: root.updateDisplay(root.txOffset, root.txMax); ColumnLayout { id: pageRoot anchors.topMargin: 40 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 MoneroComponents.Label { fontSize: 24 text: qsTr("Transactions") + translationManager.emptyString } Item { Layout.fillWidth: true } RowLayout { id: sortAndFilter visible: root.txCount > 0 property bool collapsed: false Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.preferredWidth: 100 Layout.preferredHeight: 15 spacing: 8 MoneroComponents.TextPlain { Layout.alignment: Qt.AlignVCenter font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Sort & filter") + translationManager.emptyString color: MoneroComponents.Style.defaultFontColor MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { sortAndFilter.collapsed = !sortAndFilter.collapsed } } } MoneroEffects.ImageMask { id: sortCollapsedIcon Layout.alignment: Qt.AlignVCenter height: 8 width: 12 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 rotation: sortAndFilter.collapsed ? 0 : 180 color: MoneroComponents.Style.defaultFontColor MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true onClicked: { sortAndFilter.collapsed = !sortAndFilter.collapsed } } } } } ColumnLayout { Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin visible: sortAndFilter.collapsed MoneroComponents.LineEdit { id: searchInput Layout.fillWidth: true input.topPadding: 6 input.bottomPadding: 6 fontSize: 16 labelFontSize: 14 placeholderText: qsTr("Search...") + translationManager.emptyString placeholderFontSize: 16 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 Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin columns: 2 columnSpacing: 20 MoneroComponents.DatePicker { id: fromDatePicker Layout.fillWidth: true width: 100 inputLabel.text: qsTr("Date from") + translationManager.emptyString inputLabel.font.pixelSize: 14 onCurrentDateChanged: { if(root.initialized){ root.reset(); root.updateFilter(); } } } MoneroComponents.DatePicker { id: toDatePicker Layout.fillWidth: true width: 100 inputLabel.text: qsTr("Date to") + translationManager.emptyString onCurrentDateChanged: { if(root.initialized){ root.reset(); root.updateFilter(); } } } } RowLayout { Layout.topMargin: 20 Layout.bottomMargin: 20 Layout.fillWidth: true Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin Rectangle { visible: sortAndFilter.collapsed color: "transparent" Layout.preferredWidth: childrenRect.width + 38 Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Sort by") + ":" + translationManager.emptyString color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { visible: sortAndFilter.collapsed id: sortBlockheight color: "transparent" Layout.preferredWidth: sortBlockheightText.width + 42 Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent MoneroComponents.TextPlain { id: sortBlockheightText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Blockheight") + translationManager.emptyString color: root.sortBy === "blockheight" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor themeTransition: false } MoneroEffects.ImageMask { height: 8 width: 12 visible: root.sortBy === "blockheight" ? true : false opacity: root.sortBy === "blockheight" ? 1 : 0.2 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 color: MoneroComponents.Style.defaultFontColor 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 Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent MoneroComponents.TextPlain { id: sortDateText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Date") + translationManager.emptyString color: root.sortBy === "timestamp" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor themeTransition: false } MoneroEffects.ImageMask { height: 8 width: 12 visible: root.sortBy === "timestamp" ? true : false opacity: root.sortBy === "timestamp" ? 1 : 0.2 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 color: MoneroComponents.Style.defaultFontColor 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 Layout.preferredHeight: 20 RowLayout { clip: true anchors.fill: parent MoneroComponents.TextPlain { id: sortAmountText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Amount") + translationManager.emptyString color: root.sortBy === "amount" ? MoneroComponents.Style.defaultFontColor : MoneroComponents.Style.dimmedFontColor themeTransition: false } MoneroEffects.ImageMask { height: 8 width: 12 visible: root.sortBy === "amount" ? true : false opacity: root.sortBy === "amount" ? 1 : 0.2 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 color: MoneroComponents.Style.defaultFontColor 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 MoneroComponents.TextPlain { // status message font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: root.historyStatusMessage color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Item { Layout.fillWidth: true } RowLayout { id: pagination visible: root.txCount > 0 spacing: 0 Layout.alignment: Qt.AlignRight Layout.preferredWidth: childrenRect.width Layout.preferredHeight: 20 Rectangle { color: "transparent" Layout.preferredWidth: childrenRect.width + 2 Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Page") + ":" + translationManager.emptyString color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.preferredWidth: childrenRect.width + 10 Layout.leftMargin: 4 Layout.preferredHeight: 20 MoneroComponents.TextPlain { id: paginationText text: root.txPage + "/" + Math.ceil(root.txCount / root.txMax) color: MoneroComponents.Style.defaultFontColor 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.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 Layout.preferredHeight: 20 color: "transparent" opacity: enabled ? 1.0 : 0.2 enabled: false MoneroEffects.ImageMask { id: prevIcon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left height: 8 width: 12 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 color: MoneroComponents.Style.defaultFontColor rotation: 90 } MouseArea { enabled: parent.enabled anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { root.paginationPrevClicked(); } } } Rectangle { id: paginationNext Layout.preferredWidth: 18 Layout.preferredHeight: 20 color: "transparent" opacity: enabled ? 1.0 : 0.2 enabled: false MoneroEffects.ImageMask { id: nextIcon anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right height: 8 width: 12 image: "qrc:///images/whiteDropIndicator.png" fontAwesomeFallbackIcon: FontAwesome.arrowDown fontAwesomeFallbackSize: 14 rotation: 270 color: MoneroComponents.Style.defaultFontColor } 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: { if(!collapsed) return "transparent" return MoneroComponents.Style.blackTheme ? "#06FFFFFF" : "#04000000" } 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 anchors.horizontalCenter: parent.horizontalCenter width: 10 height: 10 radius: 8 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: isout ? qsTr("Sent") : qsTr("Received") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: displayAmount 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: isout ? qsTr("Fee") : confirmationsRequired === 60 ? qsTr("Mined") : qsTr("Fee") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Blockheight") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 14 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Confirmations") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { property bool confirmed: confirmations < confirmationsRequired ? false : true font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Date") color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: persistentSettings.historyHumanDates ? dateHuman : dateTime color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter MouseArea { state: "copyable" anchors.fill: parent hoverEnabled: true onEntered: { parent.color = MoneroComponents.Style.orange if (persistentSettings.historyHumanDates) { parent.text = dateTime; } } onExited: { parent.color = MoneroComponents.Style.defaultFontColor if (persistentSettings.historyHumanDates) { parent.text = dateHuman } } } } } 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 width: 28 MouseArea { state: "details" anchors.fill: parent hoverEnabled: true z: parent.z + 1 onEntered: parent.opacity = 0.8; onExited: parent.opacity = 1.0; } } Image { visible: !isout && confirmationsRequired === 60 anchors.left: btnDetails.right anchors.leftMargin: 16 width: 28 height: 28 source: "qrc:///images/miningxmr.png" } MoneroComponents.StandardButton { visible: isout anchors.left: btnDetails.right anchors.leftMargin: 10 text: FontAwesome.productHunt small: true label.font.family: FontAwesome.fontFamilyBrands fontSize: 18 width: 36 MouseArea { state: "proof" anchors.fill: parent hoverEnabled: true z: parent.z + 1 onEntered: parent.opacity = 0.8; onExited: parent.opacity = 1.0; } } } } } ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.preferredHeight: 40 Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Description") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { id: txNoteText font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 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 } } MoneroEffects.ImageMask { anchors.top: parent.top anchors.left: txNoteText.right anchors.leftMargin: 12 image: "qrc:///images/edit.svg" fontAwesomeFallbackIcon: FontAwesome.pencilSquare fontAwesomeFallbackSize: 22 color: MoneroComponents.Style.defaultFontColor opacity: 0.75 width: 23 height: 21 MouseArea { id: txNoteArea state: "set_tx_note" anchors.fill: parent hoverEnabled: true onEntered: parent.opacity = 0.4; onExited: parent.opacity = 0.75; cursorShape: Qt.PointingHandCursor } } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 10 } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Transaction ID") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Transaction key") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Click to reveal") color: MoneroComponents.Style.defaultFontColor anchors.verticalCenter: parent.verticalCenter state: "txkey_hidden" MouseArea { state: "copyable_txkey" 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 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: qsTr("Address sent to") + translationManager.emptyString color: MoneroComponents.Style.historyHeaderTextColor themeTransitionBlackColor: MoneroComponents.Style._b_historyHeaderTextColor themeTransitionWhiteColor: MoneroComponents.Style._w_historyHeaderTextColor anchors.verticalCenter: parent.verticalCenter } } Rectangle { visible: isout color: "transparent" Layout.fillWidth: true Layout.preferredHeight: 20 MoneroComponents.TextPlain { font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 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 === 'copyable_txkey') root.getTxKey(hash, res[i]); if(res[i].state === 'set_tx_note') root.editDescription(hash, tx_note); if(res[i].state === 'details') root.showTxDetails(hash, paymentId, destinations, subaddrAccount, subaddrIndex, dateTime, displayAmount, isout); 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" MoneroEffects.ImageMask { id: collapsedIcon anchors.top: parent.top anchors.topMargin: 24 anchors.horizontalCenter: parent.horizontalCenter height: 8 width: 12 image: "qrc:///images/whiteDropIndicator.png" rotation: delegate.collapsed ? 180 : 0 color: MoneroComponents.Style.defaultFontColor } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 1 color: MoneroComponents.Style.appWindowBorderColor MoneroEffects.ColorTransition { targetObj: parent blackColor: MoneroComponents.Style._b_appWindowBorderColor whiteColor: MoneroComponents.Style._w_appWindowBorderColor } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.bottom height: 1 color: MoneroComponents.Style.appWindowBorderColor MoneroEffects.ColorTransition { targetObj: parent blackColor: MoneroComponents.Style._b_appWindowBorderColor whiteColor: MoneroComponents.Style._w_appWindowBorderColor } } } } Item { visible: sortAndFilter.collapsed Layout.topMargin: 10 Layout.bottomMargin: 10 Layout.leftMargin: sideMargin Layout.rightMargin: sideMargin MoneroComponents.TextPlain { // status message Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 15 text: root.historyStatusMessage; color: MoneroComponents.Style.dimmedFontColor themeTransitionBlackColor: MoneroComponents.Style._b_dimmedFontColor themeTransitionWhiteColor: MoneroComponents.Style._w_dimmedFontColor } } MoneroComponents.CheckBox2 { id: showAdvancedCheckbox Layout.topMargin: 30 Layout.bottomMargin: 20 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 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(keepDate) { root.txOffset = 0; if (typeof root.model !== 'undefined' && root.model != null) { if (!keepDate) { 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 const timezoneOffset = new Date().getTimezoneOffset() * 60; var fromDate = Math.floor(fromDatePicker.currentDate.getTime() / 86400000) * 86400 + timezoneOffset; var toDate = (Math.floor(toDatePicker.currentDate.getTime() / 86400000) + 1) * 86400 + timezoneOffset; 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.toLowerCase().startsWith(root.sortSearchString.toLowerCase())){ txs.push(item); } else if(item.blockheight.toString().startsWith(root.sortSearchString)) { txs.push(item); } else if(item.tx_note.toLowerCase().indexOf(root.sortSearchString.toLowerCase()) !== -1) { 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 displayAmount = amount; if(displayAmount === 0){ // *sometimes* amount is 0, while the 'destinations string' // has the correct amount, so we try to fetch it from that instead. displayAmount = TxUtils.destinationsToAmount(destinations); displayAmount = Number(displayAmount *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), "displayAmount": displayAmount + " XMR", "hash": hash, "paymentId": paymentId, "address": address, "destinations": destinations, "tx_note": tx_note, "dateHuman": dateHuman, "dateTime": date + " " + time, "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, _tx_note){ 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(_tx_note); } 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 getTxKey(hash, elem){ if (elem.parent.state != 'ready'){ currentWallet.getTxKeyAsync(hash, function(hash, txKey) { elem.parent.text = txKey ? txKey : '-'; elem.parent.state = 'ready'; }); } else { toClipboard(elem.parent.text); } } function showTxDetails(hash, paymentId, destinations, subaddrAccount, subaddrIndex, dateTime, amount, isout) { 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) const hasPaymentId = parseInt(paymentId, 16); const integratedAddress = !isout && hasPaymentId ? currentWallet.integratedAddress(paymentId) : null; if (rings) rings = rings.replace(/\|/g, '\n') currentWallet.getTxKeyAsync(hash, function(hash, tx_key) { informationPopup.title = qsTr("Transaction details") + translationManager.emptyString; informationPopup.content = buildTxDetailsString(hash, hasPaymentId ? paymentId : null, tx_key, tx_note, destinations, rings, address, address_label, integratedAddress, dateTime, amount); 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, integratedAddress, dateTime, amount) { var trStart = '', trMiddle = '', trEnd = ""; return '' + (tx_id ? trStart + qsTr("Tx ID:") + trMiddle + tx_id + trEnd : "") + (dateTime ? trStart + qsTr("Date") + ":" + trMiddle + dateTime + trEnd : "") + (amount ? trStart + qsTr("Amount") + ":" + trMiddle + amount + trEnd : "") + (address ? trStart + qsTr("Address:") + trMiddle + address + trEnd : "") + (paymentId ? trStart + qsTr("Payment ID:") + trMiddle + paymentId + trEnd : "") + (integratedAddress ? trStart + qsTr("Integrated address") + ":" + trMiddle + integratedAddress + 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(true); } }