From 88c47d333823e3d00955a3ef25be6d2c5898d92c Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Wed, 15 Jan 2025 19:01:44 +0300 Subject: [PATCH] Add Search page to Optiions dialog --- src/base/preferences.cpp | 26 +++++ src/base/preferences.h | 6 + src/gui/optionsdialog.cpp | 22 ++++ src/gui/optionsdialog.h | 4 + src/gui/optionsdialog.ui | 84 ++++++++++++++ src/gui/search/searchwidget.cpp | 191 ++++++++++++++++++++++---------- src/gui/search/searchwidget.h | 9 +- 7 files changed, 281 insertions(+), 61 deletions(-) diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index ed9ecd316dc1..6a6b143edf70 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -655,6 +655,32 @@ void Preferences::setSearchEnabled(const bool enabled) setValue(u"Preferences/Search/SearchEnabled"_s, enabled); } +bool Preferences::storeOpenedSearchTabs() const +{ + return value(u"Search/StoreOpenedSearchTabs"_s, false); +} + +void Preferences::setStoreOpenedSearchTabs(const bool enabled) +{ + if (enabled == storeOpenedSearchTabs()) + return; + + setValue(u"Search/StoreOpenedSearchTabs"_s, enabled); +} + +bool Preferences::storeOpenedSearchTabResults() const +{ + return value(u"Search/StoreOpenedSearchTabResults"_s, false); +} + +void Preferences::setStoreOpenedSearchTabResults(const bool enabled) +{ + if (enabled == storeOpenedSearchTabResults()) + return; + + setValue(u"Search/StoreOpenedSearchTabResults"_s, enabled); +} + bool Preferences::isWebUIEnabled() const { #ifdef DISABLE_GUI diff --git a/src/base/preferences.h b/src/base/preferences.h index b1a912477bda..7e07446d6541 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -172,6 +172,12 @@ class Preferences final : public QObject bool isSearchEnabled() const; void setSearchEnabled(bool enabled); + // Search UI + bool storeOpenedSearchTabs() const; + void setStoreOpenedSearchTabs(bool enabled); + bool storeOpenedSearchTabResults() const; + void setStoreOpenedSearchTabResults(bool enabled); + // HTTP Server bool isWebUIEnabled() const; void setWebUIEnabled(bool enabled); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 676612146589..ba4ee232c8a3 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -164,6 +164,7 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent) m_ui->tabSelection->item(TAB_DOWNLOADS)->setIcon(UIThemeManager::instance()->getIcon(u"download"_s, u"folder-download"_s)); m_ui->tabSelection->item(TAB_SPEED)->setIcon(UIThemeManager::instance()->getIcon(u"speedometer"_s, u"chronometer"_s)); m_ui->tabSelection->item(TAB_RSS)->setIcon(UIThemeManager::instance()->getIcon(u"application-rss"_s, u"application-rss+xml"_s)); + m_ui->tabSelection->item(TAB_SEARCH)->setIcon(UIThemeManager::instance()->getIcon(u"edit-find"_s)); #ifdef DISABLE_WEBUI m_ui->tabSelection->item(TAB_WEBUI)->setHidden(true); #else @@ -190,6 +191,7 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent) loadSpeedTabOptions(); loadBittorrentTabOptions(); loadRSSTabOptions(); + loadSearchTabOptions(); #ifndef DISABLE_WEBUI loadWebUITabOptions(); #endif @@ -1273,6 +1275,25 @@ void OptionsDialog::saveRSSTabOptions() const autoDownloader->setDownloadRepacks(m_ui->checkSmartFilterDownloadRepacks->isChecked()); } +void OptionsDialog::loadSearchTabOptions() +{ + const auto *pref = Preferences::instance(); + + m_ui->groupStoreOpenedTabs->setChecked(pref->storeOpenedSearchTabs()); + m_ui->checkStoreTabsSearchResults->setChecked(pref->storeOpenedSearchTabResults()); + + connect(m_ui->groupStoreOpenedTabs, &QGroupBox::toggled, this, &OptionsDialog::enableApplyButton); + connect(m_ui->checkStoreTabsSearchResults, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton); +} + +void OptionsDialog::saveSearchTabOptions() const +{ + auto *pref = Preferences::instance(); + + pref->setStoreOpenedSearchTabs(m_ui->groupStoreOpenedTabs->isChecked()); + pref->setStoreOpenedSearchTabResults(m_ui->checkStoreTabsSearchResults->isChecked()); +} + #ifndef DISABLE_WEBUI void OptionsDialog::loadWebUITabOptions() { @@ -1465,6 +1486,7 @@ void OptionsDialog::saveOptions() const saveSpeedTabOptions(); saveBittorrentTabOptions(); saveRSSTabOptions(); + saveSearchTabOptions(); #ifndef DISABLE_WEBUI saveWebUITabOptions(); #endif diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h index 534ca4078ef8..795ec3bf36a8 100644 --- a/src/gui/optionsdialog.h +++ b/src/gui/optionsdialog.h @@ -74,6 +74,7 @@ class OptionsDialog final : public GUIApplicationComponent TAB_SPEED, TAB_BITTORRENT, TAB_RSS, + TAB_SEARCH, TAB_WEBUI, TAB_ADVANCED }; @@ -136,6 +137,9 @@ private slots: void loadRSSTabOptions(); void saveRSSTabOptions() const; + void loadSearchTabOptions(); + void saveSearchTabOptions() const; + #ifndef DISABLE_WEBUI void loadWebUITabOptions(); void saveWebUITabOptions() const; diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 16484c71705e..140e4c31d84c 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -77,6 +77,11 @@ RSS + + + Search + + WebUI @@ -3404,6 +3409,85 @@ Disable encryption: Only connect to peers without protocol encryption + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + 0 + 0 + 521 + 541 + + + + + + + Search UI + + + + + + Store opened tabs + + + true + + + false + + + + + + Also store search results + + + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 422 + + + + + + + + + + diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp index 6e1a43c07733..2c486b7f9cac 100644 --- a/src/gui/search/searchwidget.cpp +++ b/src/gui/search/searchwidget.cpp @@ -34,10 +34,6 @@ #include -#ifdef Q_OS_WIN -#include -#endif - #include #include #include @@ -55,6 +51,7 @@ #include "base/global.h" #include "base/logger.h" +#include "base/preferences.h" #include "base/profile.h" #include "base/search/searchhandler.h" #include "base/search/searchpluginmanager.h" @@ -73,7 +70,21 @@ #define URL_COLUMN 5 const QString DATA_FOLDER_NAME = u"SearchUI"_s; -const QString STATE_FILE_NAME = u"Session.json"_s; +const QString SESSION_FILE_NAME = u"Session.json"_s; + +const QString KEY_SESSION_TABS = u"Tabs"_s; +const QString KEY_SESSION_CURRENTTAB = u"CurrentTab"_s; +const QString KEY_TAB_ID = u"ID"_s; +const QString KEY_TAB_SEARCHPATTERN = u"SearchPattern"_s; +const QString KEY_RESULT_FILENAME = u"FileName"_s; +const QString KEY_RESULT_FILEURL = u"FileURL"_s; +const QString KEY_RESULT_FILESIZE = u"FileSize"_s; +const QString KEY_RESULT_SEEDERSCOUNT = u"SeedersCount"_s; +const QString KEY_RESULT_LEECHERSCOUNT = u"LeechersCount"_s; +const QString KEY_RESULT_ENGINENAME = u"EngineName"_s; +const QString KEY_RESULT_SITEURL = u"SiteURL"_s; +const QString KEY_RESULT_DESCRLINK = u"DescrLink"_s; +const QString KEY_RESULT_PUBDATE = u"PubDate"_s; namespace { @@ -127,42 +138,42 @@ namespace const QJsonObject resultObj = resultVal.toObject(); SearchResult &searchResult = searchResults.emplaceBack(); - if (const QJsonValue fileNameVal = resultObj[u"FileName"]; fileNameVal.isString()) + if (const QJsonValue fileNameVal = resultObj[KEY_RESULT_FILENAME]; fileNameVal.isString()) searchResult.fileName = fileNameVal.toString(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue fileURLVal = resultObj[u"FileURL"]; fileURLVal.isString()) + if (const QJsonValue fileURLVal = resultObj[KEY_RESULT_FILEURL]; fileURLVal.isString()) searchResult.fileUrl= fileURLVal.toString(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue fileSizeVal = resultObj[u"FileSize"]; fileSizeVal.isDouble()) + if (const QJsonValue fileSizeVal = resultObj[KEY_RESULT_FILESIZE]; fileSizeVal.isDouble()) searchResult.fileSize= fileSizeVal.toInteger(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue seedersCountVal = resultObj[u"SeedersCount"]; seedersCountVal.isDouble()) + if (const QJsonValue seedersCountVal = resultObj[KEY_RESULT_SEEDERSCOUNT]; seedersCountVal.isDouble()) searchResult.nbSeeders = seedersCountVal.toInteger(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue leechersCountVal = resultObj[u"LeechersCount"]; leechersCountVal.isDouble()) + if (const QJsonValue leechersCountVal = resultObj[KEY_RESULT_LEECHERSCOUNT]; leechersCountVal.isDouble()) searchResult.nbLeechers = leechersCountVal.toInteger(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue siteURLVal = resultObj[u"SiteURL"]; siteURLVal.isString()) + if (const QJsonValue siteURLVal = resultObj[KEY_RESULT_SITEURL]; siteURLVal.isString()) searchResult.siteUrl= siteURLVal.toString(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue descrLinkVal = resultObj[u"DescrLink"]; descrLinkVal.isString()) + if (const QJsonValue descrLinkVal = resultObj[KEY_RESULT_DESCRLINK]; descrLinkVal.isString()) searchResult.descrLink= descrLinkVal.toString(); else return nonstd::make_unexpected(u"Invalid data format."_s); - if (const QJsonValue pubDateVal = resultObj[u"PubDate"]; pubDateVal.isDouble()) + if (const QJsonValue pubDateVal = resultObj[KEY_RESULT_PUBDATE]; pubDateVal.isDouble()) searchResult.pubDate = QDateTime::fromSecsSinceEpoch(pubDateVal.toInteger()); else return nonstd::make_unexpected(u"Invalid data format."_s); @@ -209,7 +220,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent) #endif connect(m_ui->tabWidget, &QTabWidget::tabCloseRequested, this, &SearchWidget::closeTab); connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::currentTabChanged); - connect(m_ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, &SearchWidget::saveState); + connect(m_ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, &SearchWidget::saveSession); connect(m_ui->tabWidget, &QTabWidget::tabBarDoubleClicked, this, [this](const int tabIndex) { @@ -257,7 +268,10 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent) const auto *focusSearchHotkeyAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), this); connect(focusSearchHotkeyAlternative, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits); - restoreState(); + configure(); + connect(Preferences::instance(), &Preferences::changed, this, &SearchWidget::configure); + + restoreSession(); } bool SearchWidget::eventFilter(QObject *object, QEvent *event) @@ -291,6 +305,44 @@ bool SearchWidget::eventFilter(QObject *object, QEvent *event) return QWidget::eventFilter(object, event); } +void SearchWidget::configure() +{ + const auto *pref = Preferences::instance(); + + const bool storeOpenedTabs = pref->storeOpenedSearchTabs(); + const bool isStoreOpenedTabsChanged = storeOpenedTabs != m_storeOpenedTabs; + if (isStoreOpenedTabsChanged) + { + m_storeOpenedTabs = storeOpenedTabs; + if (m_storeOpenedTabs) + { + // TODO: Store all tabs. + } + else + { + // TODO: Delete all stored tabs. + } + } + + + const bool storeOpenedTabsResults = pref->storeOpenedSearchTabResults(); + const bool isStoreOpenedTabsResultsChanged = storeOpenedTabsResults != m_storeOpenedTabsResults; + if (isStoreOpenedTabsResultsChanged) + m_storeOpenedTabsResults = storeOpenedTabsResults; + + if (isStoreOpenedTabsResultsChanged || isStoreOpenedTabsChanged) + { + if (m_storeOpenedTabsResults) + { + // TODO: Store all tabs results. + } + else + { + // TODO: Delete all stored tabs results. + } + } +} + void SearchWidget::fillCatCombobox() { m_ui->comboCategory->clear(); @@ -387,15 +439,15 @@ void SearchWidget::saveSearchResults(SearchJobWidget *searchJobWidget) const for (const SearchResult &searchResult : asConst(searchJobWidget->searchResults())) { searchResultsArray.append(QJsonObject { - {u"FileName"_s, searchResult.fileName}, - {u"FileURL"_s, searchResult.fileUrl}, - {u"FileSize"_s, searchResult.fileSize}, - {u"SeedersCount"_s, searchResult.nbSeeders}, - {u"LeechersCount"_s, searchResult.nbLeechers}, - {u"EngineName"_s, searchResult.engineName}, - {u"SiteURL"_s, searchResult.siteUrl}, - {u"DescrLink"_s, searchResult.descrLink}, - {u"PubDate"_s, Utils::DateTime::toSecsSinceEpoch(searchResult.pubDate)} + {KEY_RESULT_FILENAME, searchResult.fileName}, + {KEY_RESULT_FILEURL, searchResult.fileUrl}, + {KEY_RESULT_FILESIZE, searchResult.fileSize}, + {KEY_RESULT_SEEDERSCOUNT, searchResult.nbSeeders}, + {KEY_RESULT_LEECHERSCOUNT, searchResult.nbLeechers}, + {KEY_RESULT_ENGINENAME, searchResult.engineName}, + {KEY_RESULT_SITEURL, searchResult.siteUrl}, + {KEY_RESULT_DESCRLINK, searchResult.descrLink}, + {KEY_RESULT_PUBDATE, Utils::DateTime::toSecsSinceEpoch(searchResult.pubDate)} }); } @@ -409,8 +461,11 @@ void SearchWidget::saveSearchResults(SearchJobWidget *searchJobWidget) const } } -void SearchWidget::saveState() const +void SearchWidget::saveSession() const { + if (!m_storeOpenedTabs) + return; + QJsonArray tabsList; for (int tabIndex = 0; tabIndex < m_ui->tabWidget->count(); ++tabIndex) { @@ -422,57 +477,61 @@ void SearchWidget::saveState() const tabsList.append(tabObj); } - const QJsonObject stateObj { - {u"Tabs"_s, tabsList} + const QJsonObject sessionObj { + {u"Tabs"_s, tabsList}, + {u"CurrentTab"_s, m_ui->tabWidget->currentIndex()} }; - const Path stateFilePath = makeDataFilePath(STATE_FILE_NAME); - const auto saveResult = Utils::IO::saveToFile(stateFilePath, QJsonDocument(stateObj).toJson()); + const Path sessionFilePath = makeDataFilePath(SESSION_FILE_NAME); + const auto saveResult = Utils::IO::saveToFile(sessionFilePath, QJsonDocument(sessionObj).toJson()); if (!saveResult) { LogMsg(tr("Failed to save Search UI state. File: \"%1\". Error: \"%2\"") - .arg(stateFilePath.toString(), saveResult.error()), Log::WARNING); + .arg(sessionFilePath.toString(), saveResult.error()), Log::WARNING); } } -void SearchWidget::restoreState() +void SearchWidget::restoreSession() { + if (!m_storeOpenedTabs) + return; + const int fileMaxSize = 10 * 1024 * 1024; - const Path stateFilePath = makeDataFilePath(STATE_FILE_NAME); - const auto readResult = Utils::IO::readFile(stateFilePath, fileMaxSize); + const Path sessionFilePath = makeDataFilePath(SESSION_FILE_NAME); + const auto readResult = Utils::IO::readFile(sessionFilePath, fileMaxSize); if (!readResult) { if (readResult.error().status != Utils::IO::ReadError::NotExist) { LogMsg(tr("Failed to read Search UI saved state. File: \"%1\". Error: \"%2\"") - .arg(stateFilePath.toString(), readResult.error().message), Log::WARNING); + .arg(sessionFilePath.toString(), readResult.error().message), Log::WARNING); } return; } QJsonParseError jsonError; - const QJsonDocument stateDoc = QJsonDocument::fromJson(readResult.value(), &jsonError); + const QJsonDocument sessionDoc = QJsonDocument::fromJson(readResult.value(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { LogMsg(tr("Failed to parse Search UI saved state data. File: \"%1\". Error: \"%2\"") - .arg(stateFilePath.toString(), jsonError.errorString()), Log::WARNING); + .arg(sessionFilePath.toString(), jsonError.errorString()), Log::WARNING); return; } - if (!stateDoc.isObject()) + if (!sessionDoc.isObject()) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } - const QJsonObject stateObj = stateDoc.object(); - const QJsonValue tabsVal = stateObj[u"Tabs"]; + const QJsonObject sessionObj = sessionDoc.object(); + const QJsonValue tabsVal = sessionObj[KEY_SESSION_TABS]; if (!tabsVal.isArray()) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } @@ -482,25 +541,25 @@ void SearchWidget::restoreState() if (!tabVal.isObject()) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } const QJsonObject tabObj = tabVal.toObject(); - const QJsonValue tabIDVal = tabObj[u"ID"]; + const QJsonValue tabIDVal = tabObj[KEY_TAB_ID]; if (!tabIDVal.isString()) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } - const QJsonValue patternVal = tabObj[u"SearchPattern"]; + const QJsonValue patternVal = tabObj[KEY_TAB_SEARCHPATTERN]; if (!patternVal.isString()) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } @@ -508,31 +567,44 @@ void SearchWidget::restoreState() if (m_tabIDs.contains(tabID)) { LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Detected duplicate tab ID.\"") - .arg(stateFilePath.toString()), Log::WARNING); + .arg(sessionFilePath.toString()), Log::WARNING); return; } tabs[tabID] = patternVal.toString(); } + const QJsonValue currentTabVal = sessionObj[KEY_SESSION_CURRENTTAB]; + if (!currentTabVal.isDouble()) + { + LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"Invalid data format.\"") + .arg(sessionFilePath.toString()), Log::WARNING); + return; + } + for (const auto &[tabID, searchPattern] : tabs.asKeyValueRange()) { QList searchResults; - const Path tabStateFilePath = makeDataFilePath(tabID + u".json"); - if (const auto loadTabStateResult = loadSearchResults(tabStateFilePath)) + if (m_storeOpenedTabsResults) { - searchResults = loadTabStateResult.value(); - } - else - { - LogMsg(tr("Failed to load saved search results. Tab: \"%1\". File: \"%2\". Error: \"%3\"") - .arg(searchPattern, tabStateFilePath.toString(), loadTabStateResult.error()), Log::WARNING); + const Path tabStateFilePath = makeDataFilePath(tabID + u".json"); + if (const auto loadTabStateResult = loadSearchResults(tabStateFilePath)) + { + searchResults = loadTabStateResult.value(); + } + else + { + LogMsg(tr("Failed to load saved search results. Tab: \"%1\". File: \"%2\". Error: \"%3\"") + .arg(searchPattern, tabStateFilePath.toString(), loadTabStateResult.error()), Log::WARNING); + } } auto *restoredTab = new SearchJobWidget(tabID, searchPattern, searchResults, app(), this); addTab(tabID, restoredTab); } + + m_ui->tabWidget->setCurrentIndex(currentTabVal.toInt()); } void SearchWidget::selectActivePage() @@ -693,7 +765,7 @@ void SearchWidget::searchButtonClicked() addTab(newTabID, newTab); m_ui->tabWidget->setCurrentWidget(newTab); tabStatusChanged(newTab); - saveState(); + saveSession(); } void SearchWidget::stopButtonClicked() @@ -715,7 +787,8 @@ void SearchWidget::tabStatusChanged(SearchJobWidget *tab) emit searchFinished(tab->status() == SearchJobWidget::Status::Error); - saveSearchResults(tab); + if (m_storeOpenedTabsResults) + saveSearchResults(tab); } } @@ -727,7 +800,7 @@ void SearchWidget::closeTab(const int index) delete tab; Utils::Fs::removeFile(makeDataFilePath(tabID + u".json")); - saveState(); + saveSession(); } void SearchWidget::closeAllTabs() @@ -741,7 +814,7 @@ void SearchWidget::closeAllTabs() delete tab; } - saveState(); + saveSession(); } void SearchWidget::refreshTab(SearchJobWidget *searchJobWidget) diff --git a/src/gui/search/searchwidget.h b/src/gui/search/searchwidget.h index d2aa1524355a..7987ae323b76 100644 --- a/src/gui/search/searchwidget.h +++ b/src/gui/search/searchwidget.h @@ -64,6 +64,8 @@ class SearchWidget : public GUIApplicationComponent private: bool eventFilter(QObject *object, QEvent *event) override; + void configure(); + void pluginsButtonClicked(); void searchButtonClicked(); void stopButtonClicked(); @@ -93,11 +95,14 @@ class SearchWidget : public GUIApplicationComponent Path makeDataFilePath(const QString &fileName) const; void saveSearchResults(SearchJobWidget *searchJobWidget) const; - void saveState() const; - void restoreState(); + void saveSession() const; + void restoreSession(); Ui::SearchWidget *m_ui = nullptr; QPointer m_currentSearchTab; // Selected tab bool m_isNewQueryString = false; QSet m_tabIDs; + + bool m_storeOpenedTabs = false; + bool m_storeOpenedTabsResults = false; };