diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 882a061..324ea90 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -315,6 +315,7 @@ void MainWindow::initMenu() { // [Wallet] -> [History] connect(ui->actionExport_CSV, &QAction::triggered, this, &MainWindow::onExportHistoryCSV); + connect(ui->actionImportHistoryCSV, &QAction::triggered, this, &MainWindow::onImportHistoryDescriptionsCSV); // [Wallet] -> [Contacts] connect(ui->actionExportContactsCSV, &QAction::triggered, this, &MainWindow::onExportContactsCSV); @@ -575,7 +576,7 @@ void MainWindow::onWalletOpened() { m_wallet->history()->refresh(); }); // Vice versa - connect(m_wallet->history(), &TransactionHistory::txNoteChanged, [this] { + connect(m_wallet->transactionHistoryModel(), &TransactionHistoryModel::transactionDescriptionChanged, [this] { m_wallet->coins()->refresh(); }); @@ -1713,6 +1714,21 @@ void MainWindow::onExportHistoryCSV() { Utils::showInfo(this, "CSV export", QString("Transaction history exported to %1").arg(fn)); } +void MainWindow::onImportHistoryDescriptionsCSV() { + const QString fileName = QFileDialog::getOpenFileName(this, "Import CSV file", QDir::homePath(), "CSV Files (*.csv)"); + if (fileName.isEmpty()) { + return; + } + + QString error = m_wallet->history()->importLabelsFromCSV(fileName); + if (!error.isEmpty()) { + Utils::showError(this, "Unable to import transaction descriptions from CSV", error); + } + else { + Utils::showInfo(this, "Successfully imported transaction descriptions from CSV"); + } +} + void MainWindow::onExportContactsCSV() { auto *model = m_wallet->addressBookModel(); if (model->rowCount() <= 0){ diff --git a/src/MainWindow.h b/src/MainWindow.h index 9e15b0c..84cbe41 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -113,6 +113,7 @@ private slots: void menuToggleTabVisible(const QString &key); void menuClearHistoryClicked(); void onExportHistoryCSV(); + void onImportHistoryDescriptionsCSV(); void onExportContactsCSV(); void onCreateDesktopEntry(); void onShowDocumentation(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 7cde519..1133416 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -499,6 +499,7 @@ History + @@ -939,6 +940,11 @@ Tx pool viewer + + + Import descriptions from CSV + + diff --git a/src/libwalletqt/Coins.cpp b/src/libwalletqt/Coins.cpp index 7ab8a45..c5de0aa 100644 --- a/src/libwalletqt/Coins.cpp +++ b/src/libwalletqt/Coins.cpp @@ -34,6 +34,8 @@ CoinsInfo* Coins::coin(int index) void Coins::refresh() { + qDebug() << Q_FUNC_INFO; + emit refreshStarted(); boost::shared_lock transfers_lock(m_wallet2->m_transfers_mutex); diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index 064352d..7102f3e 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -64,6 +64,8 @@ TransactionRow* TransactionHistory::transaction(int index) void TransactionHistory::refresh() { + qDebug() << Q_FUNC_INFO; + QDateTime firstDateTime = QDate(2014, 4, 18).startOfDay(); QDateTime lastDateTime = QDateTime::currentDateTime().addDays(1); // tomorrow (guard against jitter and timezones) @@ -281,12 +283,14 @@ void TransactionHistory::refresh() void TransactionHistory::setTxNote(const QString &txid, const QString ¬e) { cryptonote::blobdata txid_data; - if(!epee::string_tools::parse_hexstr_to_binbuff(txid.toStdString(), txid_data) || txid_data.size() != sizeof(crypto::hash)) + if (!epee::string_tools::parse_hexstr_to_binbuff(txid.toStdString(), txid_data) || txid_data.size() != sizeof(crypto::hash)) { + qDebug() << Q_FUNC_INFO << "invalid txid"; return; + } + const crypto::hash htxid = *reinterpret_cast(txid_data.data()); m_wallet2->set_tx_note(htxid, note.toStdString()); - refresh(); emit txNoteChanged(); } @@ -401,4 +405,102 @@ bool TransactionHistory::writeCSV(const QString &path) { data = QString("blockHeight,timestamp,date,accountIndex,direction,balanceDelta,amount,fee,txid,description,paymentId,fiatAmount,fiatCurrency%1").arg(data); return Utils::fileWrite(path, data); -} \ No newline at end of file +} + +QStringList parseCSVLine(const QString &line) { + QStringList result; + QString currentField; + bool inQuotes = false; + + for (int i = 0; i < line.length(); ++i) { + QChar currentChar = line[i]; + + if (currentChar == '"') { + if (inQuotes && i + 1 < line.length() && line[i + 1] == '"') { + currentField.append('"'); + ++i; + } else { + inQuotes = !inQuotes; + } + } else if (currentChar == ',' && !inQuotes) { + result.append(currentField.trimmed()); + currentField.clear(); + } else { + currentField.append(currentChar); + } + } + + result.append(currentField.trimmed()); + return result; +} + +QString TransactionHistory::importLabelsFromCSV(const QString &fileName) { + QFile file(fileName); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return QString("Could not open file: %1").arg(fileName); + } + + QTextStream in(&file); + + QList fields; + while (!in.atEnd()) { + QString line = in.readLine(); + fields.append(parseCSVLine(line)); + } + + if (fields.empty()) { + return "CSV file appears to be empty"; + } + + qint64 txidField = -1; + qint64 descriptionField = -1; + + QStringList header = fields[0]; + for (int i = 0; i < header.length(); i++) { + if (header[i] == "txid") { + txidField = i; + continue; + } + if (header[i] == "description") { + descriptionField = i; + } + } + + if (txidField < 0) { + return "'txid' field not found in CSV header"; + } + if (descriptionField < 0) { + return "'description' field not found in CSV header"; + } + qint64 maxIndex = std::max(txidField, descriptionField); + + QList> descriptions; + + for (int i = 1; i < fields.length(); i++) { + const auto& row = fields[i]; + if (maxIndex >= row.length()) { + qDebug() << "Row with invalid length in CSV"; + continue; + } + + if (row[txidField].isEmpty()) { + continue; + } + + if (row[descriptionField].isEmpty()) { + continue; + } + + descriptions.push_back({row[txidField], row[descriptionField]}); + } + + for (const auto& description : descriptions) { + qDebug() << "Setting note for tx:" << description.first << "description:" << description.second; + this->setTxNote(description.first, description.second); + } + + this->refresh(); + + return {}; +} diff --git a/src/libwalletqt/TransactionHistory.h b/src/libwalletqt/TransactionHistory.h index ce10b8e..7b0d5b4 100644 --- a/src/libwalletqt/TransactionHistory.h +++ b/src/libwalletqt/TransactionHistory.h @@ -34,7 +34,6 @@ public: TransactionRow* transaction(int index); void refresh(); void setTxNote(const QString &txid, const QString ¬e); - bool writeCSV(const QString &path); quint64 count() const; QDateTime firstDateTime() const; QDateTime lastDateTime() const; @@ -42,6 +41,9 @@ public: bool locked() const; void clearRows(); + bool writeCSV(const QString &path); + QString importLabelsFromCSV(const QString &fileName); + signals: void refreshStarted() const; void refreshFinished() const; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 770718a..ce5f494 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -74,10 +74,6 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) this->updateBalance(); } - connect(this->history(), &TransactionHistory::txNoteChanged, [this]{ - this->history()->refresh(); - }); - connect(this, &Wallet::refreshed, this, &Wallet::onRefreshed); connect(this, &Wallet::newBlock, this, &Wallet::onNewBlock); connect(this, &Wallet::updated, this, &Wallet::onUpdated); diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index 932b43d..5e87563 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -236,6 +236,8 @@ bool TransactionHistoryModel::setData(const QModelIndex &index, const QVariant & hash = tInfo.hash(); }); m_transactionHistory->setTxNote(hash, value.toString()); + m_transactionHistory->refresh(); + emit transactionDescriptionChanged(); break; } default: diff --git a/src/model/TransactionHistoryModel.h b/src/model/TransactionHistoryModel.h index d84a6d7..38f467d 100644 --- a/src/model/TransactionHistoryModel.h +++ b/src/model/TransactionHistoryModel.h @@ -45,6 +45,7 @@ public: signals: void transactionHistoryChanged(); + void transactionDescriptionChanged(); private: QVariant parseTransactionInfo(const TransactionRow &tInfo, int column, int role) const;