mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-07-12 16:23:07 -07:00
parent
f8536162f2
commit
b76054beba
6 changed files with 246 additions and 7 deletions
|
@ -655,6 +655,21 @@ void Preferences::setSearchEnabled(const bool enabled)
|
|||
setValue(u"Preferences/Search/SearchEnabled"_s, enabled);
|
||||
}
|
||||
|
||||
int Preferences::searchHistoryLength() const
|
||||
{
|
||||
const int val = value(u"Search/HistoryLength"_s, 50);
|
||||
return std::clamp(val, 0, 99);
|
||||
}
|
||||
|
||||
void Preferences::setSearchHistoryLength(const int length)
|
||||
{
|
||||
const int clampedLength = std::clamp(length, 0, 99);
|
||||
if (clampedLength == searchHistoryLength())
|
||||
return;
|
||||
|
||||
setValue(u"Search/HistoryLength"_s, clampedLength);
|
||||
}
|
||||
|
||||
bool Preferences::storeOpenedSearchTabs() const
|
||||
{
|
||||
return value(u"Search/StoreOpenedSearchTabs"_s, false);
|
||||
|
|
|
@ -173,6 +173,8 @@ public:
|
|||
void setSearchEnabled(bool enabled);
|
||||
|
||||
// Search UI
|
||||
int searchHistoryLength() const;
|
||||
void setSearchHistoryLength(int length);
|
||||
bool storeOpenedSearchTabs() const;
|
||||
void setStoreOpenedSearchTabs(bool enabled);
|
||||
bool storeOpenedSearchTabResults() const;
|
||||
|
|
|
@ -1281,9 +1281,11 @@ void OptionsDialog::loadSearchTabOptions()
|
|||
|
||||
m_ui->groupStoreOpenedTabs->setChecked(pref->storeOpenedSearchTabs());
|
||||
m_ui->checkStoreTabsSearchResults->setChecked(pref->storeOpenedSearchTabResults());
|
||||
m_ui->searchHistoryLengthSpinBox->setValue(pref->searchHistoryLength());
|
||||
|
||||
connect(m_ui->groupStoreOpenedTabs, &QGroupBox::toggled, this, &OptionsDialog::enableApplyButton);
|
||||
connect(m_ui->checkStoreTabsSearchResults, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
|
||||
connect(m_ui->searchHistoryLengthSpinBox, qSpinBoxValueChanged, this, &OptionsDialog::enableApplyButton);
|
||||
}
|
||||
|
||||
void OptionsDialog::saveSearchTabOptions() const
|
||||
|
@ -1292,6 +1294,7 @@ void OptionsDialog::saveSearchTabOptions() const
|
|||
|
||||
pref->setStoreOpenedSearchTabs(m_ui->groupStoreOpenedTabs->isChecked());
|
||||
pref->setStoreOpenedSearchTabResults(m_ui->checkStoreTabsSearchResults->isChecked());
|
||||
pref->setSearchHistoryLength(m_ui->searchHistoryLengthSpinBox->value());
|
||||
}
|
||||
|
||||
#ifndef DISABLE_WEBUI
|
||||
|
|
|
@ -3272,6 +3272,43 @@ Disable encryption: Only connect to peers without protocol encryption</string>
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="searchHistoryLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="searchHistoryLengthLabel">
|
||||
<property name="text">
|
||||
<string>History length</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="searchHistoryLengthSpinBox">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::ButtonSymbols::PlusMinus</enum>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>99</number>
|
||||
</property>
|
||||
<property name="stepType">
|
||||
<enum>QAbstractSpinBox::StepType::DefaultStepType</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="searchHistoryLengthSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
#include <utility>
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QEvent>
|
||||
#include <QJsonArray>
|
||||
|
@ -48,6 +49,9 @@
|
|||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QShortcut>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStringList>
|
||||
#include <QStringListModel>
|
||||
#include <QThread>
|
||||
|
||||
#include "base/global.h"
|
||||
|
@ -56,6 +60,8 @@
|
|||
#include "base/profile.h"
|
||||
#include "base/search/searchhandler.h"
|
||||
#include "base/search/searchpluginmanager.h"
|
||||
#include "base/utils/bytearray.h"
|
||||
#include "base/utils/compare.h"
|
||||
#include "base/utils/datetime.h"
|
||||
#include "base/utils/fs.h"
|
||||
#include "base/utils/foreignapps.h"
|
||||
|
@ -67,7 +73,12 @@
|
|||
#include "searchjobwidget.h"
|
||||
#include "ui_searchwidget.h"
|
||||
|
||||
const int HISTORY_FILE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
const int SESSION_FILE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
const int RESULTS_FILE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
const QString DATA_FOLDER_NAME = u"SearchUI"_s;
|
||||
const QString HISTORY_FILE_NAME = u"History.txt"_s;
|
||||
const QString SESSION_FILE_NAME = u"Session.json"_s;
|
||||
|
||||
const QString KEY_SESSION_TABS = u"Tabs"_s;
|
||||
|
@ -86,6 +97,24 @@ const QString KEY_RESULT_PUBDATE = u"PubDate"_s;
|
|||
|
||||
namespace
|
||||
{
|
||||
class SearchHistorySortModel final : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(SearchHistorySortModel)
|
||||
|
||||
public:
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
|
||||
private:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
|
||||
{
|
||||
const int result = m_naturalCompare(left.data(sortRole()).toString(), right.data(sortRole()).toString());
|
||||
return result < 0;
|
||||
}
|
||||
|
||||
Utils::Compare::NaturalCompare<Qt::CaseInsensitive> m_naturalCompare;
|
||||
};
|
||||
|
||||
struct TabData
|
||||
{
|
||||
QString tabID;
|
||||
|
@ -132,10 +161,27 @@ namespace
|
|||
return tabName;
|
||||
}
|
||||
|
||||
nonstd::expected<QStringList, QString> loadHistory(const Path &filePath)
|
||||
{
|
||||
const auto readResult = Utils::IO::readFile(filePath, HISTORY_FILE_MAX_SIZE);
|
||||
if (!readResult)
|
||||
{
|
||||
if (readResult.error().status == Utils::IO::ReadError::NotExist)
|
||||
return {};
|
||||
|
||||
return nonstd::make_unexpected(readResult.error().message);
|
||||
}
|
||||
|
||||
QStringList history;
|
||||
for (const QByteArrayView line : asConst(Utils::ByteArray::splitToViews(readResult.value(), "\n")))
|
||||
history.append(QString::fromUtf8(line));
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
nonstd::expected<SessionData, QString> loadSession(const Path &filePath)
|
||||
{
|
||||
const int fileMaxSize = 10 * 1024 * 1024;
|
||||
const auto readResult = Utils::IO::readFile(filePath, fileMaxSize);
|
||||
const auto readResult = Utils::IO::readFile(filePath, SESSION_FILE_MAX_SIZE);
|
||||
if (!readResult)
|
||||
{
|
||||
if (readResult.error().status == Utils::IO::ReadError::NotExist)
|
||||
|
@ -191,8 +237,7 @@ namespace
|
|||
|
||||
nonstd::expected<QList<SearchResult>, QString> loadSearchResults(const Path &filePath)
|
||||
{
|
||||
const int fileMaxSize = 10 * 1024 * 1024;
|
||||
const auto readResult = Utils::IO::readFile(filePath, fileMaxSize);
|
||||
const auto readResult = Utils::IO::readFile(filePath, RESULTS_FILE_MAX_SIZE);
|
||||
if (!readResult)
|
||||
{
|
||||
if (readResult.error().status != Utils::IO::ReadError::NotExist)
|
||||
|
@ -285,8 +330,12 @@ public:
|
|||
void removeSession();
|
||||
void storeTab(const QString &tabID, const QList<SearchResult> &searchResults);
|
||||
void removeTab(const QString &tabID);
|
||||
void loadHistory();
|
||||
void storeHistory(const QStringList &history);
|
||||
void removeHistory();
|
||||
|
||||
signals:
|
||||
void historyLoaded(const QStringList &history);
|
||||
void sessionLoaded(const SessionData &sessionData);
|
||||
void tabLoaded(const QString &tabID, const QString &searchPattern, const QList<SearchResult> &searchResults);
|
||||
};
|
||||
|
@ -295,7 +344,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
|
|||
: GUIApplicationComponent(app, parent)
|
||||
, m_ui {new Ui::SearchWidget()}
|
||||
, m_ioThread {new QThread}
|
||||
, m_dataStorage {new DataStorage(this)}
|
||||
, m_dataStorage {new DataStorage}
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
|
@ -379,6 +428,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
|
|||
const auto *focusSearchHotkeyAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), this);
|
||||
connect(focusSearchHotkeyAlternative, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits);
|
||||
|
||||
m_historyLength = Preferences::instance()->searchHistoryLength();
|
||||
m_storeOpenedTabs = Preferences::instance()->storeOpenedSearchTabs();
|
||||
m_storeOpenedTabsResults = Preferences::instance()->storeOpenedSearchTabResults();
|
||||
connect(Preferences::instance(), &Preferences::changed, this, &SearchWidget::onPreferencesChanged);
|
||||
|
@ -388,6 +438,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
|
|||
m_ioThread->setObjectName("SearchWidget m_ioThread");
|
||||
m_ioThread->start();
|
||||
|
||||
loadHistory();
|
||||
restoreSession();
|
||||
}
|
||||
|
||||
|
@ -437,7 +488,7 @@ void SearchWidget::onPreferencesChanged()
|
|||
}
|
||||
else
|
||||
{
|
||||
QMetaObject::invokeMethod(m_dataStorage, [this] { m_dataStorage->removeSession(); });
|
||||
QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::removeSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -469,6 +520,36 @@ void SearchWidget::onPreferencesChanged()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int historyLength = pref->searchHistoryLength();
|
||||
if (historyLength != m_historyLength)
|
||||
{
|
||||
if (m_historyLength <= 0)
|
||||
{
|
||||
createSearchPatternCompleter();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (historyLength <= 0)
|
||||
{
|
||||
m_searchPatternCompleterModel->removeRows(0, m_searchPatternCompleterModel->rowCount());
|
||||
QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::removeHistory);
|
||||
}
|
||||
else if (historyLength < m_historyLength)
|
||||
{
|
||||
if (const int rowCount = m_searchPatternCompleterModel->rowCount(); rowCount > historyLength)
|
||||
{
|
||||
m_searchPatternCompleterModel->removeRows(0, (rowCount - historyLength));
|
||||
QMetaObject::invokeMethod(m_dataStorage, [this]
|
||||
{
|
||||
m_dataStorage->storeHistory(m_searchPatternCompleterModel->stringList());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_historyLength = historyLength;
|
||||
}
|
||||
}
|
||||
|
||||
void SearchWidget::fillCatCombobox()
|
||||
|
@ -552,6 +633,54 @@ int SearchWidget::addTab(const QString &tabID, SearchJobWidget *searchJobWdget)
|
|||
return m_ui->tabWidget->addTab(searchJobWdget, makeTabName(searchJobWdget));
|
||||
}
|
||||
|
||||
void SearchWidget::updateHistory(const QString &newSearchPattern)
|
||||
{
|
||||
if (m_historyLength <= 0)
|
||||
return;
|
||||
|
||||
if (m_searchPatternCompleterModel->stringList().contains(newSearchPattern))
|
||||
return;
|
||||
|
||||
const int rowNum = m_searchPatternCompleterModel->rowCount();
|
||||
m_searchPatternCompleterModel->insertRow(rowNum);
|
||||
m_searchPatternCompleterModel->setData(m_searchPatternCompleterModel->index(rowNum, 0), newSearchPattern);
|
||||
if (m_searchPatternCompleterModel->rowCount() > m_historyLength)
|
||||
m_searchPatternCompleterModel->removeRow(0);
|
||||
|
||||
QMetaObject::invokeMethod(m_dataStorage, [this, history = m_searchPatternCompleterModel->stringList()]
|
||||
{
|
||||
m_dataStorage->storeHistory(history);
|
||||
});
|
||||
}
|
||||
|
||||
void SearchWidget::loadHistory()
|
||||
{
|
||||
if (m_historyLength <= 0)
|
||||
return;
|
||||
|
||||
createSearchPatternCompleter();
|
||||
|
||||
connect(m_dataStorage, &DataStorage::historyLoaded, this, [this](const QStringList &storedHistory)
|
||||
{
|
||||
if (m_historyLength <= 0)
|
||||
return;
|
||||
|
||||
QStringList history = storedHistory;
|
||||
for (const QString &newPattern : asConst(m_searchPatternCompleterModel->stringList()))
|
||||
{
|
||||
if (!history.contains(newPattern))
|
||||
history.append(newPattern);
|
||||
}
|
||||
|
||||
if (history.size() > m_historyLength)
|
||||
history = history.mid(history.size() - m_historyLength);
|
||||
|
||||
m_searchPatternCompleterModel->setStringList(history);
|
||||
});
|
||||
|
||||
QMetaObject::invokeMethod(m_dataStorage, &SearchWidget::DataStorage::loadHistory);
|
||||
}
|
||||
|
||||
void SearchWidget::saveSession() const
|
||||
{
|
||||
if (!m_storeOpenedTabs)
|
||||
|
@ -570,6 +699,20 @@ void SearchWidget::saveSession() const
|
|||
QMetaObject::invokeMethod(m_dataStorage, [this, sessionData] { m_dataStorage->storeSession(sessionData); });
|
||||
}
|
||||
|
||||
void SearchWidget::createSearchPatternCompleter()
|
||||
{
|
||||
Q_ASSERT(!m_ui->lineEditSearchPattern->completer());
|
||||
|
||||
m_searchPatternCompleterModel = new QStringListModel(this);
|
||||
auto *sortModel = new SearchHistorySortModel(this);
|
||||
sortModel->setSourceModel(m_searchPatternCompleterModel);
|
||||
sortModel->sort(0);
|
||||
auto *completer = new QCompleter(sortModel, this);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
|
||||
m_ui->lineEditSearchPattern->setCompleter(completer);
|
||||
}
|
||||
|
||||
void SearchWidget::restoreSession()
|
||||
{
|
||||
if (!m_storeOpenedTabs)
|
||||
|
@ -750,6 +893,7 @@ void SearchWidget::searchButtonClicked()
|
|||
m_ui->tabWidget->setTabIcon(tabIndex, UIThemeManager::instance()->getIcon(statusIconName(newTab->status())));
|
||||
m_ui->tabWidget->setCurrentWidget(newTab);
|
||||
adjustSearchButton();
|
||||
updateHistory(pattern);
|
||||
saveSession();
|
||||
}
|
||||
|
||||
|
@ -918,4 +1062,34 @@ void SearchWidget::DataStorage::removeTab(const QString &tabID)
|
|||
Utils::Fs::removeFile(makeDataFilePath(tabID + u".json"));
|
||||
}
|
||||
|
||||
void SearchWidget::DataStorage::loadHistory()
|
||||
{
|
||||
const Path historyFilePath = makeDataFilePath(HISTORY_FILE_NAME);
|
||||
const auto loadResult = ::loadHistory(historyFilePath);
|
||||
if (!loadResult)
|
||||
{
|
||||
LogMsg(tr("Failed to load Search UI history. File: \"%1\". Error: \"%2\"")
|
||||
.arg(historyFilePath.toString(), loadResult.error()), Log::WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
emit historyLoaded(loadResult.value());
|
||||
}
|
||||
|
||||
void SearchWidget::DataStorage::storeHistory(const QStringList &history)
|
||||
{
|
||||
const Path filePath = makeDataFilePath(HISTORY_FILE_NAME);
|
||||
const auto saveResult = Utils::IO::saveToFile(filePath, history.join(u'\n').toUtf8());
|
||||
if (!saveResult)
|
||||
{
|
||||
LogMsg(tr("Failed to save search history. File: \"%1\". Error: \"%2\"")
|
||||
.arg(filePath.toString(), saveResult.error()), Log::WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
void SearchWidget::DataStorage::removeHistory()
|
||||
{
|
||||
Utils::Fs::removeFile(makeDataFilePath(HISTORY_FILE_NAME));
|
||||
}
|
||||
|
||||
#include "searchwidget.moc"
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
|
||||
class QEvent;
|
||||
class QObject;
|
||||
class QStringListModel;
|
||||
|
||||
class SearchJobWidget;
|
||||
|
||||
|
@ -93,8 +94,12 @@ private:
|
|||
QString generateTabID() const;
|
||||
int addTab(const QString &tabID, SearchJobWidget *searchJobWdget);
|
||||
|
||||
void saveSession() const;
|
||||
void loadHistory();
|
||||
void restoreSession();
|
||||
void updateHistory(const QString &newSearchPattern);
|
||||
void saveSession() const;
|
||||
|
||||
void createSearchPatternCompleter();
|
||||
|
||||
Ui::SearchWidget *m_ui = nullptr;
|
||||
QPointer<SearchJobWidget> m_currentSearchTab; // Selected tab
|
||||
|
@ -103,9 +108,12 @@ private:
|
|||
|
||||
bool m_storeOpenedTabs = false;
|
||||
bool m_storeOpenedTabsResults = false;
|
||||
int m_historyLength = 0;
|
||||
|
||||
Utils::Thread::UniquePtr m_ioThread;
|
||||
|
||||
class DataStorage;
|
||||
DataStorage *m_dataStorage = nullptr;
|
||||
|
||||
QStringListModel *m_searchPatternCompleterModel = nullptr;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue