diff options
| author | 2023-08-05 04:39:01 -0400 | |
|---|---|---|
| committer | 2023-08-05 10:39:01 +0200 | |
| commit | 2b021749ce4dc3ec8e160a5800e323f7a45047db (patch) | |
| tree | f81f7365808ca2c02efb847d7a3b61aad901ea4b /src/modules/logview | |
| parent | kernel: We should read the default config on startup (bis) (#2548) (diff) | |
| download | KVIrc-2b021749ce4dc3ec8e160a5800e323f7a45047db.tar.gz KVIrc-2b021749ce4dc3ec8e160a5800e323f7a45047db.tar.bz2 KVIrc-2b021749ce4dc3ec8e160a5800e323f7a45047db.zip | |
Batch export logs (#2485)
* Implement most basic functional batch export
For the future:
* The loop still needs to be made asynchronous so the process doesn't hang.
* Ideally, there should be some sort of progress window.
* There is redundant code that should be reduced.
* Make batch export asynchronous
Reminder: The comment about overwrite protection no longer applies
Todo: Progress dialog
* Avoid possible dangling pointer, improve type safety and const correctness in the process
In reality, the log exporting code has no reason to be in the GUI class to begin with. `exportLog` makes sense as a Qt slot, but `createLog` should be a member function of the `LogFile` class and operate on `this` rather than a `LogFile` argument.
A future commit should amend this, but for now I will avoid changing anything unnecessary until a working progress dialog is in place and I have tested the finished feature thoroughly.
* Add progress dialog
* Make LogViewWindow::createLog a member function of LogFile
* Reduce memory usage by sharing `LogFile`s with the GUI thread
* Correct progress dialog modality
* Collect log files directly instead of reiterating through view items later
Also adds a little documentation
* Fix batch export for paths with reserved characters
* avoid sigsegv
* fix tags removal before nickname in html export
* Fix directory selection and output path for icons in html export
---------
Co-authored-by: ctrlaltca <ctrlaltca@gmail.com>
Diffstat (limited to 'src/modules/logview')
| -rw-r--r-- | src/modules/logview/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/modules/logview/ExportOperation.cpp | 45 | ||||
| -rw-r--r-- | src/modules/logview/ExportOperation.h | 27 | ||||
| -rw-r--r-- | src/modules/logview/LogFile.cpp | 249 | ||||
| -rw-r--r-- | src/modules/logview/LogFile.h | 11 | ||||
| -rw-r--r-- | src/modules/logview/LogViewWidget.cpp | 14 | ||||
| -rw-r--r-- | src/modules/logview/LogViewWidget.h | 21 | ||||
| -rw-r--r-- | src/modules/logview/LogViewWindow.cpp | 330 | ||||
| -rw-r--r-- | src/modules/logview/LogViewWindow.h | 19 | ||||
| -rw-r--r-- | src/modules/logview/libkvilogview.cpp | 33 |
10 files changed, 441 insertions, 309 deletions
diff --git a/src/modules/logview/CMakeLists.txt b/src/modules/logview/CMakeLists.txt index e7790b13a..4fb6601bb 100644 --- a/src/modules/logview/CMakeLists.txt +++ b/src/modules/logview/CMakeLists.txt @@ -5,6 +5,7 @@ set(kvilogview_SRCS LogFile.cpp LogViewWidget.cpp LogViewWindow.cpp + ExportOperation.cpp ) set(kvi_module_name kvilogview) diff --git a/src/modules/logview/ExportOperation.cpp b/src/modules/logview/ExportOperation.cpp new file mode 100644 index 000000000..49c9f47be --- /dev/null +++ b/src/modules/logview/ExportOperation.cpp @@ -0,0 +1,45 @@ +#include "ExportOperation.h" + +#include <QProgressDialog> +#include <QFutureWatcher> +#include <QVector> +#include <QtConcurrent> + +#include "LogFile.h" +#include "LogViewWindow.h" +#include "KviFileUtils.h" + +ExportOperation::ExportOperation(const std::vector<std::shared_ptr<LogFile>> & logs, LogFile::ExportType type, QString szDir, QObject * parent) + : QObject(parent) + , m_logs(logs) + , m_type(type) + , m_szDir(szDir) +{ +} + +void ExportOperation::start() +{ + QProgressDialog * pProgressDialog = new QProgressDialog("Exporting logs...", "Cancel", 0, m_logs.size()); + QFutureWatcher<void> * pFutureWatcher = new QFutureWatcher<void>(); + + QObject::connect(pFutureWatcher, &QFutureWatcher<void>::finished, pProgressDialog, &QProgressDialog::deleteLater); + QObject::connect(pFutureWatcher, &QFutureWatcher<void>::finished, pFutureWatcher, &QFutureWatcher<void>::deleteLater); + QObject::connect(pFutureWatcher, &QFutureWatcher<void>::finished, this, &ExportOperation::deleteLater); + + QObject::connect(pProgressDialog, &QProgressDialog::canceled, pFutureWatcher, &QFutureWatcher<void>::cancel); + QObject::connect(pFutureWatcher, &QFutureWatcher<void>::progressValueChanged, pProgressDialog, &QProgressDialog::setValue); + + // The directory string and the export type could be captured by value if + // this function was inlined. However, because the QtConcurrent functions + // aside from QtConcurrent::run only operate on references the list of + // pointers might expire, hence the purpose of this class. + pFutureWatcher->setFuture(QtConcurrent::map(m_logs, [this](const std::shared_ptr<LogFile> & pLog) { + const QString szDate = pLog->date().toString("yyyy.MM.dd"); + QString filename = QString("%1_%2.%3_%4").arg(pLog->typeString(), pLog->name(), pLog->network(), szDate); + filename.replace(QRegExp("[\\\\/:*?\"<>|]"), "_"); + QString szLog = m_szDir + KVI_PATH_SEPARATOR_CHAR + filename; + KviFileUtils::adjustFilePath(szLog); + pLog->createLog(m_type, szLog); + })); + pProgressDialog->show(); +} diff --git a/src/modules/logview/ExportOperation.h b/src/modules/logview/ExportOperation.h new file mode 100644 index 000000000..bb531b5dd --- /dev/null +++ b/src/modules/logview/ExportOperation.h @@ -0,0 +1,27 @@ +#ifndef _EXPORTOPERATION_H_ +#define _EXPORTOPERATION_H_ + +#include "LogFile.h" + +#include <QObject> + +#include <vector> +#include <memory> + +// ExportOperation is a small container class for the data necessary +// to export log files. The purpose of ExportOperation is to ensure the +// lifetime of that data--in particular, the list of log files. +class ExportOperation : public QObject +{ + Q_OBJECT + + std::vector<std::shared_ptr<LogFile>> m_logs; + const QString m_szDir; + const LogFile::ExportType m_type; + +public: + ExportOperation(const std::vector<std::shared_ptr<LogFile>> & logs, LogFile::ExportType type, QString szDir, QObject * parent = nullptr); + void start(); +}; + +#endif diff --git a/src/modules/logview/LogFile.cpp b/src/modules/logview/LogFile.cpp index be244077b..0c15b27be 100644 --- a/src/modules/logview/LogFile.cpp +++ b/src/modules/logview/LogFile.cpp @@ -24,12 +24,20 @@ #include "LogFile.h" +#include "kvi_settings.h" +#include "KviApplication.h" +#include "KviControlCodes.h" #include "KviQString.h" #include "KviCString.h" +#include "KviHtmlGenerator.h" +#include "KviIconManager.h" +#include "KviLocale.h" #include "KviOptions.h" #include "KviFileUtils.h" #include <QFileInfo> +#include <QDir> +#include <QTextStream> #include <QLocale> #ifdef COMPILE_ZLIB_SUPPORT @@ -144,7 +152,7 @@ LogFile::LogFile(const QString & szName) } } -void LogFile::getText(QString & szText) +void LogFile::getText(QString & szText) const { QString szLogName = fileName(); QFile logFile; @@ -189,3 +197,242 @@ void LogFile::getText(QString & szText) } #endif } + +void LogFile::createLog(ExportType exportType, QString szLog, QString * pszFile) const +{ + QRegExp rx; + QString szLogDir, szInputBuffer, szOutputBuffer, szLine, szTmp; + QString szDate = date().toString("yyyy.MM.dd"); + + /* Save export directory - this directory path is also used in the HTML export + * and info is used when working with pszFile */ + QFileInfo info(szLog); + szLogDir = info.absoluteDir().absolutePath() + KVI_PATH_SEPARATOR_CHAR; + + /* Reading in log file - LogFiles are read in as bytes, so '\r' isn't + * sanitised by default */ + getText(szInputBuffer); + QStringList lines = szInputBuffer.replace('\r', "").split('\n'); + + switch(exportType) + { + case LogFile::PlainText: + { + /* Only append extension if it isn't there already (e.g. a specific + * file is to be overwritten) */ + if(!szLog.endsWith(".txt")) + szLog += ".txt"; + + // Scan the file + for(auto & line : lines) + { + szTmp = line; + szLine = KviControlCodes::stripControlBytes(szTmp); + + // Remove icons' code + rx.setPattern("^\\d{1,3}\\s"); + szLine.replace(rx, ""); + + // Remove link from a user speaking, deal with (and keep) various ranks + // e.g.: <!ncHelLViS69> --> <HelLViS69> + rx.setPattern("\\s<([+%@&~!]?)!nc"); + szLine.replace(rx, " <\\1"); + + // Remove link from a nick in a mask + // e.g.: !nFoo [~bar@!hfoo.bar] --> Foo [~bar@!hfoo.bar] + rx.setPattern("\\s!n"); + szLine.replace(rx, " "); + + // Remove link from a host in a mask + // e.g.: Foo [~bar@!hfoo.bar] --> Foo [~bar@foo.bar] + rx.setPattern("@!h"); + szLine.replace(rx, "@"); + + // Remove link from a channel + // e.g.: !c#KVIrc --> #KVIrc + rx.setPattern("!c#"); + szLine.replace(rx, "#"); + + szOutputBuffer += szLine; + szOutputBuffer += "\n"; + } + + break; + } + case LogFile::HTML: + { + /* Only append extension if it isn't there already (e.g. a specific + * file is to be overwritten) */ + if(!szLog.endsWith(".html")) + szLog += ".html"; + + szTmp = QString("KVIrc %1 %2").arg(KVI_VERSION).arg(KVI_RELEASE_NAME); + QString szNick = ""; + bool bFirstLine = true; + + QString szTitle; + switch(type()) + { + case LogFile::Channel: + szTitle = __tr2qs_ctx("Channel %1 on %2", "log").arg(name(), network()); + break; + case LogFile::Console: + szTitle = __tr2qs_ctx("Console on %1", "log").arg(network()); + break; + case LogFile::Query: + szTitle = __tr2qs_ctx("Query with: %1 on %2", "log").arg(name(), network()); + break; + case LogFile::DccChat: + szTitle = __tr2qs_ctx("DCC Chat with: %1", "log").arg(name()); + break; + case LogFile::Other: + szTitle = __tr2qs_ctx("Something on: %1", "log").arg(network()); + break; + } + + // Prepare HTML document + szOutputBuffer += "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"; + szOutputBuffer += "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\">\n"; + szOutputBuffer += "<head>\n"; + szOutputBuffer += "\t<meta http-equiv=\"content-type\" content=\"application/xhtml+xml; charset=utf-8\" />\n"; + szOutputBuffer += "\t<meta name=\"author\" content=\"" + szTmp + "\" />\n"; + szOutputBuffer += "\t<title>" + szTitle + "</title>\n"; + szOutputBuffer += "</head>\n<body>\n"; + szOutputBuffer += "<h2>" + szTitle + "</h2>\n<h3>Date: " + szDate + "</h3>\n"; + + // Scan the file + for(auto & line : lines) + { + szTmp = line; + + // Find who has talked + QString szTmpNick = szTmp.section(" ", 2, 2); + if((szTmpNick.left(1) != "<") && (szTmpNick.right(1) != ">")) + szTmpNick = ""; + + // locate msgtype + QString szNum = szTmp.section(' ', 0, 0); + bool bOk; + int iMsgType = szNum.toInt(&bOk); + + // only human text for now... + if(iMsgType != 24 && iMsgType != 25 && iMsgType != 26) + continue; + + // remove msgtype tag + szTmp = szTmp.remove(0, szNum.length() + 1); + + szTmp = KviHtmlGenerator::convertToHtml(szTmp, true); + + // insert msgtype icon at start of the current text line + KviMessageTypeSettings msg(KVI_OPTION_MSGTYPE(iMsgType)); + QString szIcon = g_pIconManager->getSmallIconResourceName((KviIconManager::SmallIcon)msg.pixId()); + szTmp.prepend("<img src=\"" + szIcon + R"(" alt="" /> )"); + + /* + * Check if the nick who has talked is the same of the above line. + * If so, we have to put the line as it is, otherwise we have to + * open a new paragraph + */ + if(szTmpNick != szNick) + { + /* + * People is not the same, close the paragraph opened + * before and open a new one + */ + if(!bFirstLine) + szOutputBuffer += "</p>\n"; + szTmp.prepend("<p>"); + + szNick = szTmpNick; + } + else + { + // Break the line + szTmp.prepend("<br />\n"); + } + + // remove internal tags before nickname + rx.setPattern(">([+%@&~!]?)!nc"); + szTmp.replace(rx, ">\\1"); + szTmp.replace(">&!nc", ">&"); + + szOutputBuffer += szTmp; + bFirstLine = false; + } + + // Close the last paragraph + szOutputBuffer += "</p>\n"; + + // regexp to search all embedded icons + rx.setPattern("<img src=\"smallicons:([^\"]+)"); + int iIndex = szOutputBuffer.indexOf(rx); + QStringList szImagesList; + + // search for icons + while(iIndex >= 0) + { + int iLength = rx.matchedLength(); + QString szCap = rx.cap(1); + + // if the icon isn't in the images list then add + if(szImagesList.indexOf(szCap) == -1) + szImagesList.append(szCap); + iIndex = szOutputBuffer.indexOf(rx, iIndex + iLength); + } + + // get current theme path + QString szCurrentThemePath; + g_pApp->getLocalKvircDirectory(szCurrentThemePath, KviApplication::Themes, KVI_OPTION_STRING(KviOption_stringIconThemeSubdir)); + szCurrentThemePath += KVI_PATH_SEPARATOR_CHAR; + + // current coresmall path + szCurrentThemePath += "coresmall"; + szCurrentThemePath += KVI_PATH_SEPARATOR_CHAR; + + // check if coresmall exists in current theme + if(!KviFileUtils::directoryExists(szCurrentThemePath)) + { + // get global coresmall path + g_pApp->getGlobalKvircDirectory(szCurrentThemePath, KviApplication::Pics, "coresmall"); + KviQString::ensureLastCharIs(szCurrentThemePath, QChar(KVI_PATH_SEPARATOR_CHAR)); + } + + // copy all icons to the log destination folder + for(int i = 0; i < szImagesList.count(); i++) + { + QString szSourceFile = szCurrentThemePath + szImagesList.at(i); + QString szDestFile = szLogDir + szImagesList.at(i); + KviFileUtils::copyFile(szSourceFile, szDestFile); + } + + // remove internal tags + rx.setPattern("<qt>|</qt>|smallicons:"); + szOutputBuffer.replace(rx, ""); + + // Close the document + szOutputBuffer += "</body>\n</html>\n"; + + break; + } + } + + // File overwriting already dealt with when file path was obtained + QFile log(szLog); + if(!log.open(QIODevice::WriteOnly | QIODevice::Text)) + return; + + if(pszFile) + { + *pszFile = ""; + *pszFile = info.filePath(); + } + + // Ensure we're writing in UTF-8 + QTextStream output(&log); + output.setCodec("UTF-8"); + output << szOutputBuffer; + + // Close file descriptors + log.close(); +} diff --git a/src/modules/logview/LogFile.h b/src/modules/logview/LogFile.h index 9b4984aa7..eb0b4ce64 100644 --- a/src/modules/logview/LogFile.h +++ b/src/modules/logview/LogFile.h @@ -143,7 +143,16 @@ public: * \param szText The buffer where to save the contents of the log * \return void */ - void getText(QString & szText); + void getText(QString & szText) const; + + /** + * \brief Exports the log and creates the file in the selected format + * \param exportType The type of file to export the log as. Either PlainText or HTML. + * \param szLog The absolute path of the file to be created + * \param pszFile The buffer to store the exported log name + * \return void + */ + void createLog(ExportType exportType, QString szLog, QString * pszFile = nullptr) const; }; #endif // _LOGFILE_H_ diff --git a/src/modules/logview/LogViewWidget.cpp b/src/modules/logview/LogViewWidget.cpp index cf90ffa74..ed190a324 100644 --- a/src/modules/logview/LogViewWidget.cpp +++ b/src/modules/logview/LogViewWidget.cpp @@ -38,14 +38,18 @@ #include <zlib.h> #endif -LogListViewItem::LogListViewItem(QTreeWidgetItem * pPar, LogFile::Type eType, LogFile * pLog) - : QTreeWidgetItem(pPar), m_eType(eType), m_pFileData(pLog) +LogListViewItem::LogListViewItem(QTreeWidgetItem * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog) + : QTreeWidgetItem(pPar) + , m_eType(eType) + , m_pFileData(pLog) { setText(0, m_pFileData ? m_pFileData->name() : QString()); } -LogListViewItem::LogListViewItem(QTreeWidget * pPar, LogFile::Type eType, LogFile * pLog) - : QTreeWidgetItem(pPar), m_eType(eType), m_pFileData(pLog) +LogListViewItem::LogListViewItem(QTreeWidget * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog) + : QTreeWidgetItem(pPar) + , m_eType(eType) + , m_pFileData(pLog) { setText(0, m_pFileData ? m_pFileData->name() : QString()); } @@ -89,7 +93,7 @@ LogListViewItemType::LogListViewItemType(QTreeWidget * pPar, LogFile::Type eType setText(0, szText); } -LogListViewLog::LogListViewLog(QTreeWidgetItem * pPar, LogFile::Type eType, LogFile * pLog) +LogListViewLog::LogListViewLog(QTreeWidgetItem * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog) : LogListViewItem(pPar, eType, pLog) { setText(0, m_pFileData->date().toString("yyyy-MM-dd")); diff --git a/src/modules/logview/LogViewWidget.h b/src/modules/logview/LogViewWidget.h index 9092dae84..203d05d90 100644 --- a/src/modules/logview/LogViewWidget.h +++ b/src/modules/logview/LogViewWidget.h @@ -31,19 +31,21 @@ #include <QTreeWidget> +#include <memory> + class LogListViewItem : public QTreeWidgetItem { public: - LogListViewItem(QTreeWidgetItem * pPar, LogFile::Type eType, LogFile * pLog); - LogListViewItem(QTreeWidget * pPar, LogFile::Type eType, LogFile * pLog); - ~LogListViewItem(){}; + LogListViewItem(QTreeWidgetItem * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog); + LogListViewItem(QTreeWidget * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog); + ~LogListViewItem() {}; public: LogFile::Type m_eType; - LogFile * m_pFileData; + std::shared_ptr<LogFile> m_pFileData; public: - LogFile * log() { return m_pFileData; }; + std::weak_ptr<LogFile> log() { return m_pFileData; }; virtual QString fileName() const { return QString(); }; }; @@ -51,7 +53,7 @@ class LogListViewItemFolder : public LogListViewItem { public: LogListViewItemFolder(QTreeWidgetItem * pPar, const QString & szLabel); - ~LogListViewItemFolder(){}; + ~LogListViewItemFolder() {}; public: }; @@ -60,15 +62,16 @@ class LogListViewItemType : public LogListViewItem { public: LogListViewItemType(QTreeWidget * pPar, LogFile::Type eType); - ~LogListViewItemType(){}; + ~LogListViewItemType() {}; }; class LogListViewLog : public LogListViewItem { public: - LogListViewLog(QTreeWidgetItem * pPar, LogFile::Type eType, LogFile * pLog); - ~LogListViewLog(){}; + LogListViewLog(QTreeWidgetItem * pPar, LogFile::Type eType, std::shared_ptr<LogFile> pLog); + ~LogListViewLog() {}; virtual QString fileName() const { return m_pFileData->fileName(); }; + protected: bool operator<(const QTreeWidgetItem & other) const { diff --git a/src/modules/logview/LogViewWindow.cpp b/src/modules/logview/LogViewWindow.cpp index 902339cb4..754e6bef5 100644 --- a/src/modules/logview/LogViewWindow.cpp +++ b/src/modules/logview/LogViewWindow.cpp @@ -41,6 +41,8 @@ #include "KviFileDialog.h" #include "KviControlCodes.h" +#include "ExportOperation.h" + #include <QList> #include <QFileInfo> #include <QDir> @@ -53,10 +55,12 @@ #include <QMouseEvent> #include <QMessageBox> #include <QProgressBar> +#include <QProgressDialog> #include <QTextStream> #include <QTabWidget> #include <QCheckBox> #include <QMenu> +#include <QtConcurrent> #include <climits> //for INT_MAX @@ -268,14 +272,14 @@ void LogViewWindow::recurseDirectory(const QString & szDir) } else if((info.suffix() == "gz") || (info.suffix() == "log")) { - m_logList.append(new LogFile(info.filePath())); + m_logList.emplace_back(new LogFile(info.filePath())); } } } void LogViewWindow::setupItemList() { - if(m_logList.isEmpty()) + if(m_logList.empty()) return; m_pFilterButton->setEnabled(false); @@ -283,12 +287,12 @@ void LogViewWindow::setupItemList() m_bAborted = false; m_pBottomLayout->setVisible(true); - m_pProgressBar->setRange(0, m_logList.count()); + m_pProgressBar->setRange(0, m_logList.size()); m_pProgressBar->setValue(0); m_pLastCategory = nullptr; m_pLastGroupItem = nullptr; - m_logList.first(); + m_currentLog = m_logList.begin(); m_pTimer->start(); //singleshot } @@ -305,8 +309,9 @@ void LogViewWindow::abortFilter() void LogViewWindow::filterNext() { QString szCurGroup; - LogFile * pFile = m_logList.current(); - if(!pFile) + std::shared_ptr<LogFile> pFile = *m_currentLog; + + if(m_currentLog == m_logList.end()) goto filter_last; if(pFile->type() == LogFile::Channel && !m_pShowChannelsCheck->isChecked()) @@ -386,10 +391,10 @@ void LogViewWindow::filterNext() new LogListViewLog(m_pLastGroupItem, pFile->type(), pFile); filter_next: - pFile = m_logList.next(); + ++m_currentLog; filter_last: - if(pFile && !m_bAborted) + if((m_currentLog != m_logList.end()) && !m_bAborted) { m_pProgressBar->setValue(m_pProgressBar->value() + 1); m_pTimer->start(); //singleshot @@ -451,7 +456,6 @@ void LogViewWindow::rightButtonClicked(QTreeWidgetItem * pItem, const QPoint &) QMenu * pPopup = new QMenu(this); if(((LogListViewItem *)pItem)->childCount()) { - // TODO: probably should allow to specify a directory instead of asking for file path on each log file pPopup->addAction(*(g_pIconManager->getSmallIcon(KviIconManager::Save)), __tr2qs_ctx("Export All Log Files to", "log"))->setMenu(m_pExportLogPopup); pPopup->addAction(*(g_pIconManager->getSmallIcon(KviIconManager::Discard)), __tr2qs_ctx("Remove All Log Files Within This Folder", "log"), this, SLOT(deleteCurrent())); } @@ -534,12 +538,12 @@ void LogViewWindow::exportLog(QAction * pAction) * sent with the old activated signal - the ordinal is now stored as * QAction user data */ if(pAction) - exportLog(pAction->data().toInt()); + exportLog(static_cast<LogFile::ExportType>(pAction->data().toInt())); else qDebug("LogViewWindow::exportLog called with invalid pAction"); } -void LogViewWindow::exportLog(int iId) +void LogViewWindow::exportLog(LogFile::ExportType exportType) { LogListViewItem * pItem = (LogListViewItem *)(m_pListView->currentItem()); if(!pItem) @@ -547,20 +551,43 @@ void LogViewWindow::exportLog(int iId) if(!pItem->childCount()) { + std::shared_ptr<LogFile> pLog { pItem->log() }; + + QString szDate = pLog->date().toString("yyyy.MM.dd"); + + QString szLog = KVI_OPTION_STRING(KviOption_stringLogsExportPath).trimmed(); + if(!szLog.isEmpty()) + szLog += KVI_PATH_SEPARATOR_CHAR; + szLog += QString("%1_%2.%3_%4").arg(pLog->typeString(), pLog->name(), pLog->network(), szDate); + KviFileUtils::adjustFilePath(szLog); + + // Getting output file path from the user, with overwrite confirmation + if(!KviFileDialog::askForSaveFileName( + szLog, + __tr2qs_ctx("Export Log - KVIrc", "log"), + szLog, + QString(), + false, + true, + true, + this)) + return; + // Export the log - createLog(pItem->log(), iId); + pLog->createLog(exportType, szLog); return; } // We selected a node in the log list, scan the children - KviPointerList<LogListViewItem> logList; - logList.setAutoDelete(false); + std::vector<std::shared_ptr<LogFile>> logList; for(int i = 0; i < pItem->childCount(); i++) { if(!pItem->child(i)->childCount()) { // The child is a log file, append it to the list - logList.append((LogListViewItem *)pItem->child(i)); + LogListViewItem * pViewItem = static_cast<LogListViewItem *>(pItem->child(i)); + std::shared_ptr<LogFile> pLog { pViewItem->log() }; + logList.push_back(pLog); continue; } @@ -575,275 +602,26 @@ void LogViewWindow::exportLog(int iId) } // Add the child to the list - logList.append((LogListViewItem *)pChild->child(j)); + LogListViewItem * pViewItem = static_cast<LogListViewItem *>(pItem->child(j)); + std::shared_ptr<LogFile> pLog { pViewItem->log() }; + logList.push_back(pLog); } } - // Scan the list - for(unsigned int u = 0; u < logList.count(); u++) - { - LogListViewItem * pCurItem = logList.at(u); - createLog(pCurItem->log(), iId); - } -} - -void LogViewWindow::createLog(LogFile * pLog, int iId, QString * pszFile) -{ - if(!pLog) - return; - - QRegExp rx; - QString szLog, szLogDir, szInputBuffer, szOutputBuffer, szLine, szTmp; - QString szDate = pLog->date().toString("yyyy.MM.dd"); - - /* Fetching previous export path and concatenating with generated filename - * adjustFilePath is for file paths not directory paths */ - szLog = KVI_OPTION_STRING(KviOption_stringLogsExportPath).trimmed(); - if(!szLog.isEmpty()) - szLog += KVI_PATH_SEPARATOR_CHAR; - szLog += QString("%1_%2.%3_%4").arg(pLog->typeString(), pLog->name(), pLog->network(), szDate); - KviFileUtils::adjustFilePath(szLog); - - // Getting output file path from the user, with overwrite confirmation - if(!KviFileDialog::askForSaveFileName( - szLog, + // Select output directory + QString szDir = KVI_OPTION_STRING(KviOption_stringLogsExportPath).trimmed(); + if(!KviFileDialog::askForDirectoryName( + szDir, __tr2qs_ctx("Export Log - KVIrc", "log"), - szLog, + szDir, QString(), false, true, - true, this)) return; + KVI_OPTION_STRING(KviOption_stringLogsExportPath) = szDir; - /* Save export directory - this directory path is also used in the HTML export - * and info is used when working with pszFile */ - QFileInfo info(szLog); - szLogDir = info.absoluteDir().absolutePath(); - KVI_OPTION_STRING(KviOption_stringLogsExportPath) = szLogDir; - - /* Reading in log file - LogFiles are read in as bytes, so '\r' isn't - * sanitised by default */ - pLog->getText(szInputBuffer); - QStringList lines = szInputBuffer.replace('\r', "").split('\n'); - - switch(iId) - { - case LogFile::PlainText: - { - /* Only append extension if it isn't there already (e.g. a specific - * file is to be overwritten) */ - if(!szLog.endsWith(".txt")) - szLog += ".txt"; - - // Scan the file - for(auto & line : lines) - { - szTmp = line; - szLine = KviControlCodes::stripControlBytes(szTmp); - - // Remove icons' code - rx.setPattern("^\\d{1,3}\\s"); - szLine.replace(rx, ""); - - // Remove link from a user speaking, deal with (and keep) various ranks - // e.g.: <!ncHelLViS69> --> <HelLViS69> - rx.setPattern("\\s<([+%@&~!]?)!nc"); - szLine.replace(rx, " <\\1"); - - // Remove link from a nick in a mask - // e.g.: !nFoo [~bar@!hfoo.bar] --> Foo [~bar@!hfoo.bar] - rx.setPattern("\\s!n"); - szLine.replace(rx, " "); - - // Remove link from a host in a mask - // e.g.: Foo [~bar@!hfoo.bar] --> Foo [~bar@foo.bar] - rx.setPattern("@!h"); - szLine.replace(rx, "@"); - - // Remove link from a channel - // e.g.: !c#KVIrc --> #KVIrc - rx.setPattern("!c#"); - szLine.replace(rx, "#"); - - szOutputBuffer += szLine; - szOutputBuffer += "\n"; - } - - break; - } - case LogFile::HTML: - { - /* Only append extension if it isn't there already (e.g. a specific - * file is to be overwritten) */ - if(!szLog.endsWith(".html")) - szLog += ".html"; - - szTmp = QString("KVIrc %1 %2").arg(KVI_VERSION).arg(KVI_RELEASE_NAME); - QString szNick = ""; - bool bFirstLine = true; - - QString szTitle; - switch(pLog->type()) - { - case LogFile::Channel: - szTitle = __tr2qs_ctx("Channel %1 on %2", "log").arg(pLog->name(), pLog->network()); - break; - case LogFile::Console: - szTitle = __tr2qs_ctx("Console on %1", "log").arg(pLog->network()); - break; - case LogFile::Query: - szTitle = __tr2qs_ctx("Query with: %1 on %2", "log").arg(pLog->name(), pLog->network()); - break; - case LogFile::DccChat: - szTitle = __tr2qs_ctx("DCC Chat with: %1", "log").arg(pLog->name()); - break; - case LogFile::Other: - szTitle = __tr2qs_ctx("Something on: %1", "log").arg(pLog->network()); - break; - } - - // Prepare HTML document - szOutputBuffer += "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"; - szOutputBuffer += "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\">\n"; - szOutputBuffer += "<head>\n"; - szOutputBuffer += "\t<meta http-equiv=\"content-type\" content=\"application/xhtml+xml; charset=utf-8\" />\n"; - szOutputBuffer += "\t<meta name=\"author\" content=\"" + szTmp + "\" />\n"; - szOutputBuffer += "\t<title>" + szTitle + "</title>\n"; - szOutputBuffer += "</head>\n<body>\n"; - szOutputBuffer += "<h2>" + szTitle + "</h2>\n<h3>Date: " + szDate + "</h3>\n"; - - // Scan the file - for(auto & line : lines) - { - szTmp = line; - - // Find who has talked - QString szTmpNick = szTmp.section(" ", 2, 2); - if((szTmpNick.left(1) != "<") && (szTmpNick.right(1) != ">")) - szTmpNick = ""; - - // locate msgtype - QString szNum = szTmp.section(' ', 0, 0); - bool bOk; - int iMsgType = szNum.toInt(&bOk); - - // only human text for now... - if(iMsgType != 24 && iMsgType != 25 && iMsgType != 26) - continue; - - // remove msgtype tag - szTmp = szTmp.remove(0, szNum.length() + 1); - - szTmp = KviHtmlGenerator::convertToHtml(szTmp, true); - - // insert msgtype icon at start of the current text line - KviMessageTypeSettings msg(KVI_OPTION_MSGTYPE(iMsgType)); - QString szIcon = g_pIconManager->getSmallIconResourceName((KviIconManager::SmallIcon)msg.pixId()); - szTmp.prepend("<img src=\"" + szIcon + R"(" alt="" /> )"); - - /* - * Check if the nick who has talked is the same of the above line. - * If so, we have to put the line as it is, otherwise we have to - * open a new paragraph - */ - if(szTmpNick != szNick) - { - /* - * People is not the same, close the paragraph opened - * before and open a new one - */ - if(!bFirstLine) - szOutputBuffer += "</p>\n"; - szTmp.prepend("<p>"); - - szNick = szTmpNick; - } - else - { - // Break the line - szTmp.prepend("<br />\n"); - } - - szOutputBuffer += szTmp; - bFirstLine = false; - } - - // Close the last paragraph - szOutputBuffer += "</p>\n"; - - // regexp to search all embedded icons - rx.setPattern("<img src=\"smallicons:([^\"]+)"); - int iIndex = szOutputBuffer.indexOf(rx); - QStringList szImagesList; - - // search for icons - while(iIndex >= 0) - { - int iLength = rx.matchedLength(); - QString szCap = rx.cap(1); - - // if the icon isn't in the images list then add - if(szImagesList.indexOf(szCap) == -1) - szImagesList.append(szCap); - iIndex = szOutputBuffer.indexOf(rx, iIndex + iLength); - } - - // get current theme path - QString szCurrentThemePath; - g_pApp->getLocalKvircDirectory(szCurrentThemePath, KviApplication::Themes, KVI_OPTION_STRING(KviOption_stringIconThemeSubdir)); - szCurrentThemePath += KVI_PATH_SEPARATOR_CHAR; - - // current coresmall path - szCurrentThemePath += "coresmall"; - szCurrentThemePath += KVI_PATH_SEPARATOR_CHAR; - - // check if coresmall exists in current theme - if(!KviFileUtils::directoryExists(szCurrentThemePath)) - { - // get global coresmall path - g_pApp->getGlobalKvircDirectory(szCurrentThemePath, KviApplication::Pics, "coresmall"); - KviQString::ensureLastCharIs(szCurrentThemePath, QChar(KVI_PATH_SEPARATOR_CHAR)); - } - - // copy all icons to the log destination folder - for(int i = 0; i < szImagesList.count(); i++) - { - QString szSourceFile = szCurrentThemePath + szImagesList.at(i); - QString szDestFile = szLogDir + szImagesList.at(i); - KviFileUtils::copyFile(szSourceFile, szDestFile); - } - - // remove internal tags - rx.setPattern("<qt>|</qt>|smallicons:"); - szOutputBuffer.replace(rx, ""); - szOutputBuffer.replace(">!nc", ">"); - szOutputBuffer.replace("@!nc", "@"); - szOutputBuffer.replace("%!nc", "%"); - - // Close the document - szOutputBuffer += "</body>\n</html>\n"; - - break; - } - } - - // File overwriting already dealt with when file path was obtained - QFile log(szLog); - if(!log.open(QIODevice::WriteOnly | QIODevice::Text)) - return; - - if(pszFile) - { - *pszFile = ""; - *pszFile = info.filePath(); - } - - // Ensure we're writing in UTF-8 - QTextStream output(&log); - output.setCodec("UTF-8"); - output << szOutputBuffer; - - // Close file descriptors - log.close(); + // Begin asynchronously writing the logs to persistent storage + ExportOperation * worker = new ExportOperation(logList, exportType, szDir); + worker->start(); } diff --git a/src/modules/logview/LogViewWindow.h b/src/modules/logview/LogViewWindow.h index b90866b39..fb479a726 100644 --- a/src/modules/logview/LogViewWindow.h +++ b/src/modules/logview/LogViewWindow.h @@ -36,6 +36,8 @@ #include "KviPointerList.h" #include <QTreeWidget> +#include <vector> +#include <memory> class KviLogViewWidget; class LogListViewItem; @@ -52,7 +54,7 @@ class LogViewListView : public QTreeWidget Q_OBJECT public: LogViewListView(QWidget *); - ~LogViewListView(){}; + ~LogViewListView() {}; protected: void mousePressEvent(QMouseEvent * pEvent) override; @@ -68,7 +70,8 @@ public: ~LogViewWindow(); protected: - KviPointerList<LogFile> m_logList; + std::vector<std::shared_ptr<LogFile>> m_logList; + std::vector<std::shared_ptr<LogFile>>::const_iterator m_currentLog; LogViewListView * m_pListView; @@ -104,18 +107,8 @@ protected: QTimer * m_pTimer; QMenu * m_pExportLogPopup; -public: - /** - * \brief Exports the log and creates the file in the selected format - * \param pLog The log file associated with the item selected in the popup - * \param iId The id of the item in the popup - * \param pszFile The buffer to store the exported log name - * \return void - */ - void createLog(LogFile * pLog, int iId, QString * pszFile = nullptr); - protected: - void exportLog(int iId); + void exportLog(LogFile::ExportType exportType); void recurseDirectory(const QString & szDir); void setupItemList(); diff --git a/src/modules/logview/libkvilogview.cpp b/src/modules/logview/libkvilogview.cpp index d4255726e..61056f370 100644 --- a/src/modules/logview/libkvilogview.cpp +++ b/src/modules/logview/libkvilogview.cpp @@ -32,6 +32,12 @@ #include "KviIconManager.h" #include "KviLocale.h" #include "KviApplication.h" +#include "KviOptions.h" +#include "KviFileUtils.h" +#include "KviFileDialog.h" + +#include <QString> +#include <QDate> static QRect g_rectLogViewGeometry; LogViewWindow * g_pLogViewWindow = nullptr; @@ -107,12 +113,31 @@ static bool logview_module_ctrl(KviModule *, const char * pcOperation, void * pP if(!pData) return false; - LogFile log{pData->szName}; - int iId = LogFile::PlainText; + LogFile log { pData->szName }; + LogFile::ExportType exportType = LogFile::PlainText; if(pData->szType == QLatin1String("html")) - iId = LogFile::HTML; + exportType = LogFile::HTML; + + QString szDate = log.date().toString("yyyy.MM.dd"); + QString szLog = KVI_OPTION_STRING(KviOption_stringLogsExportPath).trimmed(); + if(!szLog.isEmpty()) + szLog += KVI_PATH_SEPARATOR_CHAR; + szLog += QString("%1_%2.%3_%4").arg(log.typeString(), log.name(), log.network(), szDate); + KviFileUtils::adjustFilePath(szLog); + + // Getting output file path from the user, with overwrite confirmation + if(!KviFileDialog::askForSaveFileName( + szLog, + __tr2qs_ctx("Export Log - KVIrc", "log"), + szLog, + QString(), + false, + true, + true, + g_pLogViewWindow)) + return false; - g_pLogViewWindow->createLog(&log, iId, &(pData->szFile)); + log.createLog(exportType, szLog, &(pData->szFile)); return true; } |
