-
-
- ${tracks}
- ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
-
+
+ ${tracks}
@@ -45,4 +40,13 @@
${artist class="Lms-btn Lms-artistname text-muted"}
+
+
+
+ ${elements}
+ ${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
+
+
+
+
diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt
index 1dada5af..77570c7e 100644
--- a/src/lms/CMakeLists.txt
+++ b/src/lms/CMakeLists.txt
@@ -15,21 +15,26 @@ add_executable(lms
ui/admin/UserView.cpp
ui/admin/UsersView.cpp
ui/common/DirectoryValidator.cpp
+ ui/common/InfiniteScrollingContainer.cpp
ui/common/LoadingIndicator.cpp
ui/common/LoginNameValidator.cpp
ui/common/MandatoryValidator.cpp
ui/common/PasswordValidator.cpp
ui/common/UUIDValidator.cpp
+ ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp
ui/explore/ArtistView.cpp
ui/explore/ArtistsView.cpp
+ ui/explore/DatabaseCollectorBase.cpp
ui/explore/Explore.cpp
ui/explore/Filters.cpp
+ ui/explore/ReleaseCollector.cpp
ui/explore/ReleaseListHelpers.cpp
ui/explore/ReleasePopup.cpp
ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp
ui/explore/SearchView.cpp
+ ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp
ui/explore/TrackPopup.cpp
ui/explore/TracksView.cpp
diff --git a/src/lms/ui/common/InfiniteScrollingContainer.cpp b/src/lms/ui/common/InfiniteScrollingContainer.cpp
new file mode 100644
index 00000000..954c3a28
--- /dev/null
+++ b/src/lms/ui/common/InfiniteScrollingContainer.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see
.
+ */
+
+#include "InfiniteScrollingContainer.hpp"
+
+#include "LoadingIndicator.hpp"
+
+namespace UserInterface
+{
+ InfiniteScrollingContainer::InfiniteScrollingContainer(const Wt::WString& text)
+ : Wt::WTemplate {text}
+ , _elements {bindNew
("elements")}
+ , _loadingIndicator {bindWidget("loading-indicator", createLoadingIndicator())}
+ {
+ hideLoadingIndicator();
+ }
+
+ void
+ InfiniteScrollingContainer::clear()
+ {
+ _elements->clear();
+ hideLoadingIndicator();
+ }
+
+ std::size_t
+ InfiniteScrollingContainer::getCount()
+ {
+ return _elements->count();
+ }
+
+ void
+ InfiniteScrollingContainer::add(std::unique_ptr result)
+ {
+ return _elements->addWidget(std::move(result));
+ }
+
+ void
+ InfiniteScrollingContainer::setHasMore(bool hasMore)
+ {
+ if (hasMore)
+ displayLoadingIndicator();
+ else
+ hideLoadingIndicator();
+ }
+
+ void
+ InfiniteScrollingContainer::displayLoadingIndicator()
+ {
+ _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator());
+ _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
+ {
+ if (!visible)
+ return;
+
+ onRequestElements.emit();
+ });
+ }
+
+ void
+ InfiniteScrollingContainer::hideLoadingIndicator()
+ {
+ _loadingIndicator = nullptr;
+ bindEmpty("loading-indicator");
+ }
+
+
+}
diff --git a/src/lms/ui/common/InfiniteScrollingContainer.hpp b/src/lms/ui/common/InfiniteScrollingContainer.hpp
new file mode 100644
index 00000000..48836b58
--- /dev/null
+++ b/src/lms/ui/common/InfiniteScrollingContainer.hpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace UserInterface
+{
+ class InfiniteScrollingContainer : public Wt::WTemplate
+ {
+ public:
+ // "text" must contain loading-indicator and "elements"
+ InfiniteScrollingContainer(const Wt::WString& text = Wt::WString::tr("Lms.infinite-scrolling-container"));
+
+ void clear();
+ std::size_t getCount();
+ void add(std::unique_ptr result);
+
+ void setHasMore(bool hasMore);
+
+ Wt::Signal<> onRequestElements;
+
+ private:
+ void displayLoadingIndicator();
+ void hideLoadingIndicator();
+
+ Wt::WContainerWidget* _elements;
+ Wt::WTemplate* _loadingIndicator;
+ };
+}
diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp
new file mode 100644
index 00000000..45ebf810
--- /dev/null
+++ b/src/lms/ui/explore/ArtistCollector.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "ArtistCollector.hpp"
+
+#include "database/Artist.hpp"
+#include "database/User.hpp"
+#include "database/TrackList.hpp"
+#include "scrobbling/IScrobbling.hpp"
+#include "utils/Service.hpp"
+#include "Filters.hpp"
+#include "LmsApplication.hpp"
+
+namespace UserInterface
+{
+ using namespace Database;
+
+ std::vector>
+ ArtistCollector::get(std::optional range, bool& moreResults)
+ {
+ range = getActualRange(range);
+
+ std::vector artists;
+ switch (getMode())
+ {
+ case Mode::Random:
+ artists = getRandomArtists(range, moreResults);
+ break;
+
+ case Mode::Starred:
+ artists = Artist::getStarred(LmsApp->getDbSession(),
+ LmsApp->getUser(),
+ getFilters().getClusterIds(),
+ _linkType,
+ Artist::SortMethod::BySortName,
+ range, moreResults);
+ break;
+
+ case Mode::RecentlyPlayed:
+ artists = Service::get()->getRecentArtists(LmsApp->getDbSession(), LmsApp->getUser(),
+ getFilters().getClusterIds(),
+ _linkType,
+ range, moreResults);
+ break;
+
+ case Mode::MostPlayed:
+ artists = Service::get()->getTopArtists(LmsApp->getDbSession(), LmsApp->getUser(),
+ getFilters().getClusterIds(),
+ _linkType,
+ range, moreResults);
+ break;
+
+ case Mode::RecentlyAdded:
+ artists = Artist::getLastWritten(LmsApp->getDbSession(),
+ std::nullopt, // after
+ getFilters().getClusterIds(),
+ _linkType,
+ range, moreResults);
+ break;
+
+ case Mode::Search:
+ artists = Database::Artist::getByFilter(LmsApp->getDbSession(),
+ getFilters().getClusterIds(),
+ getSearchKeywords(),
+ std::nullopt, // no link
+ Database::Artist::SortMethod::BySortName,
+ range, moreResults);
+ break;
+
+ case Mode::All:
+ artists = Artist::getByFilter(LmsApp->getDbSession(),
+ getFilters().getClusterIds(),
+ {},
+ _linkType,
+ Artist::SortMethod::BySortName,
+ range, moreResults);
+ break;
+ }
+
+ if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount()))
+ moreResults = false;
+
+ return artists;
+ }
+
+ std::vector
+ ArtistCollector::getRandomArtists(std::optional range, bool& moreResults)
+ {
+ std::vector artists;
+
+ assert(getMode() == Mode::Random);
+
+ if (_randomArtists.empty())
+ _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), _linkType, getMaxCount());
+
+ {
+ auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())};
+ auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())};
+
+ for (auto it {itBegin}; it != itEnd; ++it)
+ {
+ Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)};
+ if (artist)
+ artists.push_back(artist);
+ }
+
+ moreResults = (itEnd != std::cend(_randomArtists));
+ }
+
+ return artists;
+ }
+
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp
new file mode 100644
index 00000000..b7bb04d7
--- /dev/null
+++ b/src/lms/ui/explore/ArtistCollector.hpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "DatabaseCollectorBase.hpp"
+
+#include "database/Types.hpp"
+
+namespace Database
+{
+ class Artist;
+}
+
+namespace UserInterface
+{
+ class ArtistCollector : public DatabaseCollectorBase
+ {
+ public:
+ using DatabaseCollectorBase::DatabaseCollectorBase;
+
+ std::vector> get(std::optional range, bool& moreResults);
+ void reset() { _randomArtists.clear(); }
+ void setArtistLinkType(std::optional linkType) { _linkType = linkType; }
+
+ private:
+ std::vector> getRandomArtists(std::optional range, bool& moreResults);
+ std::vector _randomArtists;
+ std::optional _linkType;
+ };
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp
index 83e6316f..3d71b94c 100644
--- a/src/lms/ui/explore/ArtistsView.cpp
+++ b/src/lms/ui/explore/ArtistsView.cpp
@@ -19,23 +19,18 @@
#include "ArtistsView.hpp"
-#include
-
#include
-#include "common/ValueStringModel.hpp"
-#include "database/Artist.hpp"
#include "database/Session.hpp"
-#include "database/User.hpp"
#include "database/TrackArtistLink.hpp"
-#include "database/TrackList.hpp"
-#include "scrobbling/IScrobbling.hpp"
+#include "utils/EnumSet.hpp"
#include "utils/Logger.hpp"
-#include "common/LoadingIndicator.hpp"
+#include "common/ValueStringModel.hpp"
+#include "common/InfiniteScrollingContainer.hpp"
#include "ArtistListHelpers.hpp"
-#include "LmsApplication.hpp"
#include "Filters.hpp"
+#include "LmsApplication.hpp"
using namespace Database;
@@ -43,35 +38,39 @@ namespace UserInterface {
using ArtistLinkModel = ValueStringModel>;
-Artists::Artists(Filters* filters)
-: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
- _filters {filters}
+Artists::Artists(Filters& filters)
+: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
+, _artistCollector {filters, _defaultMode, _maxCount}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
{
auto* menu {bindNew("mode")};
- auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
+ auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, ArtistCollector::Mode mode)
{
auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); });
- if (mode == defaultMode)
+ if (mode == _defaultMode)
item->renderSelected(true);
};
- addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
- addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
- addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.random"), ArtistCollector::Mode::Random);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), ArtistCollector::Mode::Starred);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), ArtistCollector::Mode::RecentlyPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), ArtistCollector::Mode::MostPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), ArtistCollector::Mode::RecentlyAdded);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
}
_linkType = bindNew("link-type");
_linkType->setModel(std::make_shared());
- _linkType->changed().connect([this] { refreshView(); });
+ _linkType->changed().connect([this]
+ {
+ const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())};
+ refreshView(linkType);
+ });
refreshArtistLinkTypes();
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
@@ -80,26 +79,39 @@ Artists::Artists(Filters* filters)
refreshArtistLinkTypes();
});
- _container = bindNew("artists");
- hideLoadingIndicator();
+ _container = bindNew("artists");
+ _container->onRequestElements.connect([this]
+ {
+ addSome();
+ });
- refreshView();
+ filters.updated().connect([this]
+ {
+ refreshView();
+ });
- filters->updated().connect([this] { refreshView(); });
+ refreshView(_artistCollector.getMode());
}
void
Artists::refreshView()
{
_container->clear();
- _randomArtists.clear();
+ _artistCollector.reset();
addSome();
}
void
-Artists::refreshView(Mode mode)
+Artists::refreshView(ArtistCollector::Mode mode)
{
- _mode = mode;
+ _artistCollector.setMode(mode);
+ refreshView();
+}
+
+void
+Artists::refreshView(std::optional linkType)
+{
+ _artistCollector.setArtistLinkType(linkType);
refreshView();
}
@@ -124,7 +136,9 @@ Artists::refreshArtistLinkTypes()
linkTypeModel->clear();
- linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), {});
+ // add default one first (none)
+ linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
+
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -134,140 +148,20 @@ Artists::refreshArtistLinkTypes()
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
}
-void
-Artists::displayLoadingIndicator()
-{
- _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator());
- _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
- {
- if (!visible)
- return;
-
- addSome();
- });
-}
-
-void
-Artists::hideLoadingIndicator()
-{
- _loadingIndicator = nullptr;
- bindEmpty("loading-indicator");
-}
-
-std::vector
-Artists::getRandomArtists(std::optional range, bool& moreResults)
-{
- std::vector artists;
-
- const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())};
-
- if (_randomArtists.empty())
- _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]);
-
- {
- auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())};
- auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())};
-
- for (auto it {itBegin}; it != itEnd; ++it)
- {
- Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)};
- if (artist)
- artists.push_back(artist);
- }
-
- moreResults = (itEnd != std::cend(_randomArtists));
- }
-
- return artists;
-}
-
-std::vector
-Artists::getArtists(std::optional range, bool& moreResults)
-{
- std::vector artists;
-
- const std::optional linkType {static_cast(_linkType->model().get())->getValue(_linkType->currentIndex())};
-
- const std::optional modeLimit{maxItemsPerMode[_mode]};
- if (modeLimit)
- {
- if (range)
- range->limit = std::min(*modeLimit - range->offset, range->limit);
- else
- range = Range {0, *modeLimit};
- }
-
- switch (_mode)
- {
- case Mode::Random:
- artists = getRandomArtists(range, moreResults);
- break;
-
- case Mode::Starred:
- artists = Artist::getStarred(LmsApp->getDbSession(),
- LmsApp->getUser(),
- _filters->getClusterIds(),
- linkType,
- Artist::SortMethod::BySortName,
- range, moreResults);
- break;
-
- case Mode::RecentlyPlayed:
- artists = Service::get()->getRecentArtists(LmsApp->getDbSession(), LmsApp->getUser(),
- _filters->getClusterIds(),
- linkType,
- range, moreResults);
- break;
-
- case Mode::MostPlayed:
- artists = Service::get()->getTopArtists(LmsApp->getDbSession(), LmsApp->getUser(),
- _filters->getClusterIds(),
- linkType,
- range, moreResults);
- break;
-
- case Mode::RecentlyAdded:
- artists = Artist::getLastWritten(LmsApp->getDbSession(),
- std::nullopt,
- _filters->getClusterIds(),
- linkType,
- range, moreResults);
- break;
-
- case Mode::All:
- artists = Artist::getByFilter(LmsApp->getDbSession(),
- _filters->getClusterIds(),
- {},
- linkType,
- Artist::SortMethod::BySortName,
- range, moreResults);
- break;
-
- default:
- break;
- }
-
- if (range && modeLimit && (range->offset + range->limit == *modeLimit))
- moreResults = false;
-
- return artists;
-}
-
void
Artists::addSome()
{
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
-
bool moreResults {};
- for (const Artist::pointer& artist : getArtists(Range {static_cast(_container->count()), batchSize}, moreResults))
+
{
- _container->addWidget(ArtistListHelpers::createEntry(artist));
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+
+ const auto artists {_artistCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)};
+ for (const auto& artist : artists)
+ _container->add(ArtistListHelpers::createEntry(artist));
}
- if (moreResults)
- displayLoadingIndicator();
- else
- hideLoadingIndicator();
+ _container->setHasMore(moreResults);
}
} // namespace UserInterface
diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp
index 1c1810fe..2ad4a340 100644
--- a/src/lms/ui/explore/ArtistsView.hpp
+++ b/src/lms/ui/explore/ArtistsView.hpp
@@ -19,69 +19,39 @@
#pragma once
-#include
+#include
#include
-#include
#include
-#include
#include
#include "database/Types.hpp"
+#include "ArtistCollector.hpp"
-namespace Database
+namespace UserInterface
{
- class Artist;
-}
+ class Filters;
+ class InfiniteScrollingContainer;
-namespace UserInterface {
+ class Artists : public Wt::WTemplate
+ {
+ public:
+ Artists(Filters& filters);
-class Filters;
+ private:
+ void refreshView();
+ void refreshView(ArtistCollector::Mode mode);
+ void refreshView(std::optional linkType);
+ void refreshArtistLinkTypes();
+ void addSome();
-class Artists : public Wt::WTemplate
-{
- public:
- Artists(Filters* filters);
-
- private:
- enum class Mode
- {
- Random,
- Starred,
- RecentlyPlayed,
- RecentlyAdded,
- MostPlayed,
- All
- };
-
- void refreshView();
- void refreshView(Mode mode);
- void refreshArtistLinkTypes();
- void displayLoadingIndicator();
- void hideLoadingIndicator();
- void addSome();
-
- std::vector> getArtists(std::optional range, bool& moreResults);
- std::vector> getRandomArtists(std::optional range, bool& moreResults);
-
- static constexpr Mode defaultMode {Mode::Random};
- static constexpr std::size_t batchSize {30};
- static inline std::unordered_map> maxItemsPerMode
- {
- {Mode::Random, batchSize * 4},
- {Mode::RecentlyPlayed, batchSize * 4},
- {Mode::RecentlyAdded, batchSize * 4},
- {Mode::MostPlayed, batchSize * 4},
- {Mode::All, batchSize * 50},
- };
-
- Mode _mode {defaultMode};
- std::vector _randomArtists;
- Filters* _filters {};
- Wt::WTemplate* _loadingIndicator {};
- Wt::WContainerWidget* _container {};
- Wt::WComboBox* _linkType {};
-};
+ static constexpr std::size_t _batchSize {30};
+ static constexpr std::size_t _maxCount {128};
+ InfiniteScrollingContainer* _container {};
+ ArtistCollector _artistCollector;
+ Wt::WComboBox* _linkType {};
+ static constexpr ArtistCollector::Mode _defaultMode {ArtistCollector::Mode::Random};
+ };
} // namespace UserInterface
diff --git a/src/lms/ui/explore/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp
new file mode 100644
index 00000000..a4e9cdfc
--- /dev/null
+++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "DatabaseCollectorBase.hpp"
+
+#include "utils/String.hpp"
+
+namespace UserInterface
+{
+ DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount)
+ : _filters {filters}
+ , _mode {defaultMode}
+ , _maxCount {maxCount}
+ {
+ }
+
+ std::optional
+ DatabaseCollectorBase::getActualRange(std::optional range) const
+ {
+ if (std::optional maxCount {getMaxCount()})
+ {
+ if (range)
+ range->limit = std::min(*maxCount - range->offset, range->limit);
+ else
+ range = Range {0, *maxCount};
+ }
+
+ return range;
+ }
+
+ std::optional
+ DatabaseCollectorBase::getMaxCount() const
+ {
+ return _maxCount;
+ }
+
+ void
+ DatabaseCollectorBase::setSearch(std::string_view searchText)
+ {
+ _searchText = searchText;
+ _searchKeywords = StringUtils::splitString(_searchText, " ");
+ }
+
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp
new file mode 100644
index 00000000..224f7657
--- /dev/null
+++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "database/Types.hpp"
+
+namespace UserInterface
+{
+ class Filters;
+
+ class DatabaseCollectorBase
+ {
+ public:
+ using Range = Database::Range;
+
+ virtual ~DatabaseCollectorBase() {}
+
+ enum class Mode
+ {
+ Random,
+ Starred,
+ RecentlyPlayed,
+ RecentlyAdded,
+ MostPlayed,
+ Search,
+ All
+ };
+
+ DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount = std::nullopt);
+
+ Mode getMode() const { return _mode; }
+ void setMode(Mode mode) { _mode = mode; }
+ void setMaxCount(std::size_t maxCount) { _maxCount = maxCount; }
+ void setSearch(std::string_view search);
+
+ protected:
+ std::optional getActualRange(std::optional range) const;
+ std::optional getMaxCount() const;
+ Filters& getFilters() { return _filters; }
+ const std::vector& getSearchKeywords() const { return _searchKeywords; }
+
+ private:
+ Filters& _filters;
+ std::string _searchText;
+ std::vector _searchKeywords;
+ Mode _mode;
+ std::optional _maxCount;
+ };
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp
index 27288c05..06c32bd0 100644
--- a/src/lms/ui/explore/Explore.cpp
+++ b/src/lms/ui/explore/Explore.cpp
@@ -88,14 +88,14 @@ Explore::Explore(Filters* filters)
Wt::WStackedWidget* contentsStack = bindNew("contents");
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
- auto artists = std::make_unique(_filters);
+ auto artists = std::make_unique(*_filters);
contentsStack->addWidget(std::move(artists));
auto artist = std::make_unique(_filters);
artist->artistsAction.connect(this, &Explore::handleArtistsAction);
contentsStack->addWidget(std::move(artist));
- auto releases = std::make_unique(_filters);
+ auto releases = std::make_unique(*_filters);
releases->releasesAction.connect(this, &Explore::handleReleasesAction);
contentsStack->addWidget(std::move(releases));
@@ -109,7 +109,7 @@ Explore::Explore(Filters* filters)
_search = search.get();
contentsStack->addWidget(std::move(search));
- auto tracks = std::make_unique(_filters);
+ auto tracks = std::make_unique(*_filters);
tracks->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(tracks));
diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp
new file mode 100644
index 00000000..c9b84bb6
--- /dev/null
+++ b/src/lms/ui/explore/ReleaseCollector.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "ReleaseCollector.hpp"
+
+#include
+
+#include "database/Release.hpp"
+#include "database/User.hpp"
+#include "database/TrackList.hpp"
+#include "scrobbling/IScrobbling.hpp"
+#include "utils/Service.hpp"
+#include "Filters.hpp"
+#include "LmsApplication.hpp"
+
+namespace UserInterface
+{
+ using namespace Database;
+
+ std::vector
+ ReleaseCollector::get(std::optional range, bool& moreResults)
+ {
+ range = getActualRange(range);
+
+ std::vector releases;
+ switch (getMode())
+ {
+ case Mode::Random:
+ releases = getRandomReleases(range, moreResults);
+ break;
+
+ case Mode::Starred:
+ releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case ReleaseCollector::Mode::RecentlyPlayed:
+ releases = Service::get()->getRecentReleases(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::MostPlayed:
+ releases = Service::get()->getTopReleases(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::RecentlyAdded:
+ releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::Search:
+ releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults);
+ break;
+
+ case Mode::All:
+ releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults);
+ break;
+ }
+
+ if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount()))
+ moreResults = false;
+
+ return releases;
+ }
+
+ std::vector
+ ReleaseCollector::getAll()
+ {
+ bool moreResults;
+ const auto releases {get(std::nullopt, moreResults)};
+
+ std::vector res;
+ res.reserve(releases.size());
+ std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release.id(); });
+
+ return res;
+ }
+
+ std::vector
+ ReleaseCollector::getRandomReleases(std::optional range, bool& moreResults)
+ {
+ std::vector releases;
+
+ assert(getMode() == Mode::Random);
+
+ if (_randomReleases.empty())
+ _randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount());
+
+ {
+ auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())};
+ auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())};
+
+ for (auto it {itBegin}; it != itEnd; ++it)
+ {
+ Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)};
+ if (release)
+ releases.push_back(release);
+ }
+
+ moreResults = (itEnd != std::cend(_randomReleases));
+ }
+
+ return releases;
+ }
+
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp
new file mode 100644
index 00000000..84b57fcb
--- /dev/null
+++ b/src/lms/ui/explore/ReleaseCollector.hpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "DatabaseCollectorBase.hpp"
+
+namespace Database
+{
+ class Release;
+}
+
+namespace UserInterface
+{
+ class ReleaseCollector : public DatabaseCollectorBase
+ {
+ public:
+ using DatabaseCollectorBase::DatabaseCollectorBase;
+
+ std::vector> get(std::optional range, bool& moreResults);
+ std::vector getAll();
+ void reset() { _randomReleases.clear(); }
+
+ private:
+ std::vector> getRandomReleases(std::optional range, bool& moreResults);
+ std::vector _randomReleases;
+ };
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp
index b56a5acf..d2040366 100644
--- a/src/lms/ui/explore/ReleasesView.cpp
+++ b/src/lms/ui/explore/ReleasesView.cpp
@@ -19,23 +19,12 @@
#include "ReleasesView.hpp"
-#include
-
-#include
-#include
#include
#include
#include
-#include "database/Release.hpp"
#include "database/Session.hpp"
-#include "database/TrackList.hpp"
-#include "database/User.hpp"
-#include "scrobbling/IScrobbling.hpp"
-#include "utils/Logger.hpp"
-#include "utils/String.hpp"
-
-#include "common/LoadingIndicator.hpp"
+#include "common/InfiniteScrollingContainer.hpp"
#include "ReleaseListHelpers.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
@@ -44,30 +33,30 @@ using namespace Database;
namespace UserInterface {
-Releases::Releases(Filters* filters)
-: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")},
-_filters {filters}
+Releases::Releases(Filters& filters)
+: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")}
+, _releaseCollector {filters, _defaultMode, _maxCount}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
{
auto* menu {bindNew("mode")};
- auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
+ auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, ReleaseCollector::Mode mode)
{
auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); });
- if (mode == defaultMode)
+ if (mode == _defaultMode)
item->renderSelected(true);
};
- addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
- addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
- addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.random"), ReleaseCollector::Mode::Random);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), ReleaseCollector::Mode::Starred);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), ReleaseCollector::Mode::RecentlyPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), ReleaseCollector::Mode::MostPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), ReleaseCollector::Mode::RecentlyAdded);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.all"), ReleaseCollector::Mode::All);
}
Wt::WText* playBtn {bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
@@ -94,153 +83,55 @@ _filters {filters}
popup->popup(moreBtn);
});
- _container = bindNew("releases");
- hideLoadingIndicator();
+ _container = bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
+ _container->onRequestElements.connect([this]
+ {
+ addSome();
+ });
- refreshView(defaultMode);
+ filters.updated().connect([this]
+ {
+ refreshView();
+ });
- filters->updated().connect([this] { refreshView(); });
+ refreshView(_releaseCollector.getMode());
}
void
Releases::refreshView()
{
_container->clear();
- _randomReleases.clear();
+ _releaseCollector.reset();
addSome();
}
void
-Releases::refreshView(Mode mode)
+Releases::refreshView(ReleaseCollector::Mode mode)
{
- _mode = mode;
+ _releaseCollector.setMode(mode);
refreshView();
}
-void
-Releases::displayLoadingIndicator()
-{
- _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator());
- _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
- {
- if (!visible)
- return;
-
- addSome();
- });
-}
-
-void
-Releases::hideLoadingIndicator()
-{
- _loadingIndicator = nullptr;
- bindEmpty("loading-indicator");
-}
-
void
Releases::addSome()
{
bool moreResults {};
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
- const auto releases {getReleases(Range {static_cast(_container->count()), batchSize}, moreResults)};
-
- for (const Release::pointer& release : releases)
{
- _container->addWidget(ReleaseListHelpers::createEntry(release));
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+
+ const auto releases {_releaseCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)};
+ for (const auto& release : releases)
+ _container->add(ReleaseListHelpers::createEntry(release));
}
- if (moreResults)
- displayLoadingIndicator();
- else
- hideLoadingIndicator();
-}
-
-std::vector
-Releases::getRandomReleases(std::optional range, bool& moreResults)
-{
- std::vector releases;
-
- if (_randomReleases.empty())
- _randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
-
- {
- auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())};
- auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())};
-
- for (auto it {itBegin}; it != itEnd; ++it)
- {
- Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)};
- if (release)
- releases.push_back(release);
- }
-
- moreResults = (itEnd != std::cend(_randomReleases));
- }
-
- return releases;
-}
-
-std::vector
-Releases::getReleases(std::optional range, bool& moreResults)
-{
- std::vector releases;
-
- const std::optional modeLimit{maxItemsPerMode[_mode]};
- if (modeLimit)
- {
- if (range)
- range->limit = std::min(*modeLimit - range->offset, range->limit);
- else
- range = Range {0, *modeLimit};
- }
-
- switch (_mode)
- {
- case Mode::Random:
- releases = getRandomReleases(range, moreResults);
- break;
-
- case Mode::Starred:
- releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::RecentlyPlayed:
- releases = Service::get()->getRecentReleases(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::MostPlayed:
- releases = Service::get()->getTopReleases(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::RecentlyAdded:
- releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::All:
- releases = Release::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
- break;
- }
-
- if (range && modeLimit && (range->offset + range->limit == *modeLimit))
- moreResults = false;
-
- return releases;
+ _container->setHasMore(moreResults);
}
std::vector
Releases::getAllReleases()
{
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
-
- bool moreResults;
- const auto releases {getReleases(std::nullopt, moreResults)};
-
- std::vector res;
- res.reserve(releases.size());
- std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release.id(); });
-
- return res;
+ return _releaseCollector.getAll();
}
} // namespace UserInterface
diff --git a/src/lms/ui/explore/ReleasesView.hpp b/src/lms/ui/explore/ReleasesView.hpp
index 21e8e8ea..abcff442 100644
--- a/src/lms/ui/explore/ReleasesView.hpp
+++ b/src/lms/ui/explore/ReleasesView.hpp
@@ -19,7 +19,6 @@
#pragma once
-#include
#include
#include
@@ -27,63 +26,35 @@
#include "database/Types.hpp"
#include "PlayQueueAction.hpp"
+#include "ReleaseCollector.hpp"
-namespace Database
+namespace UserInterface
{
- class Release;
-}
+ class Filters;
+ class InfiniteScrollingContainer;
-namespace UserInterface {
+ class Releases : public Wt::WTemplate
+ {
+ public:
+ Releases(Filters& filters);
-class Filters;
+ PlayQueueActionSignal releasesAction;
-class Releases : public Wt::WTemplate
-{
- public:
- Releases(Filters* filters);
+ private:
- PlayQueueActionSignal releasesAction;
+ void refreshView();
+ void refreshView(ReleaseCollector::Mode mode);
- private:
+ void addSome();
+ std::vector getAllReleases();
- enum class Mode
- {
- Random,
- Starred,
- RecentlyPlayed,
- RecentlyAdded,
- MostPlayed,
- All
- };
-
- void refreshView();
- void refreshView(Mode mode);
- void displayLoadingIndicator();
- void hideLoadingIndicator();
-
- void addSome();
- std::vector> getReleases(std::optional range, bool& moreResults);
- std::vector> getRandomReleases(std::optional range, bool& moreResults);
- std::vector getAllReleases();
-
- static constexpr Mode defaultMode {Mode::Random};
- static constexpr std::size_t maxItemsPerLine {6};
- static constexpr std::size_t batchSize {maxItemsPerLine * 3};
- static inline std::unordered_map> maxItemsPerMode
- {
- {Mode::Random, batchSize * 10},
- {Mode::RecentlyPlayed, batchSize * 10},
- {Mode::RecentlyAdded, batchSize * 10},
- {Mode::MostPlayed, batchSize * 10},
- {Mode::All, batchSize * 30},
- };
-
- Mode _mode {defaultMode};
- Filters* _filters {};
- std::vector _randomReleases;
- Wt::WContainerWidget* _container {};
- Wt::WTemplate* _loadingIndicator {};
-};
+ static constexpr std::size_t _maxItemsPerLine {6};
+ static constexpr std::size_t _batchSize {_maxItemsPerLine};
+ static constexpr std::size_t _maxCount {_maxItemsPerLine * 24};
+ InfiniteScrollingContainer* _container {};
+ ReleaseCollector _releaseCollector;
+ static constexpr ReleaseCollector::Mode _defaultMode {ReleaseCollector::Mode::Random};
+ };
} // namespace UserInterface
diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp
index 082277eb..e7493056 100644
--- a/src/lms/ui/explore/SearchView.cpp
+++ b/src/lms/ui/explore/SearchView.cpp
@@ -19,14 +19,19 @@
#include "SearchView.hpp"
+#include
+
#include
#include
+#include
#include "database/Artist.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
+#include "common/InfiniteScrollingContainer.hpp"
+#include "common/LoadingIndicator.hpp"
#include "ArtistListHelpers.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
@@ -41,97 +46,152 @@ namespace UserInterface
SearchView::SearchView(Filters* filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
, _filters {filters}
+ , _releaseCollector {*filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
+ , _artistCollector {*filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
+ , _trackCollector {*filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
+ Wt::WStackedWidget* stack {bindNew("stack")};
+ stack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
+ _menu = bindNew("mode", stack);
+
+ auto addItem = [=](const Wt::WString& str, Mode mode, const Wt::WString& templateStr, std::function onRequestElementsFunc)
+ {
+ assert(modeToIndex(mode) == _results.size());
+
+ auto results {std::make_unique(templateStr)};
+ results->onRequestElements.connect(std::move(onRequestElementsFunc));
+
+ _results.push_back(results.get());
+ _menu->addItem(str, std::move(results));
+ };
+
+ // same order as Mode!
+ addItem(Wt::WString::tr("Lms.Explore.releases"), Mode::Release, Wt::WString::tr("Lms.Explore.Releases.template.container"), [this]{ addSomeReleases(); });
+ addItem(Wt::WString::tr("Lms.Explore.artists"), Mode::Artist, Wt::WString::tr("Lms.infinite-scrolling-container"), [this]{ addSomeArtists(); });
+ addItem(Wt::WString::tr("Lms.Explore.tracks"), Mode::Track, Wt::WString::tr("Lms.Explore.Tracks.template.container"), [this]{ addSomeTracks(); });
+
_filters->updated().connect([=]
{
refreshView();
});
+
+ refreshView();
+ }
+
+ std::size_t
+ SearchView::modeToIndex(Mode mode) const
+ {
+ return static_cast(mode);
+ }
+
+ Wt::WMenuItem&
+ SearchView::getItemMenu(Mode mode) const
+ {
+ return *_menu->itemAt(modeToIndex(mode));
+ }
+
+ InfiniteScrollingContainer&
+ SearchView::getResultContainer(Mode mode) const
+ {
+ return *_results[modeToIndex(mode)];
+ }
+
+ std::size_t
+ SearchView::getBatchSize(Mode mode) const
+ {
+ auto it {_batchSizes.find(mode)};
+ assert(it != _batchSizes.cend());
+ return it->second;
+ }
+
+ std::size_t
+ SearchView::getMaxCount(Mode mode) const
+ {
+ auto it {_maxCounts.find(mode)};
+ assert(it != _maxCounts.cend());
+ return it->second;
}
void
SearchView::refreshView(const Wt::WString& searchText)
{
- _searchValue = searchText.toUTF8();
- _keywords = StringUtils::splitString(_searchValue, " ");
+ _releaseCollector.setSearch(searchText.toUTF8());
+ _artistCollector.setSearch(searchText.toUTF8());
+ _trackCollector.setSearch(searchText.toUTF8());
refreshView();
}
void
SearchView::refreshView()
{
- clear();
+ for (InfiniteScrollingContainer* results : _results)
+ results->clear();
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
-
- searchArtists();
- searchReleases();
- searchTracks();
+ addSomeReleases();
+ addSomeArtists();
+ addSomeTracks();
}
void
- SearchView::searchArtists()
+ SearchView::addSomeArtists()
{
- bool more;
+ InfiniteScrollingContainer& results {getResultContainer(Mode::Artist)};
+ bool moreResults {};
- const auto artists {Database::Artist::getByFilter(LmsApp->getDbSession(),
- _filters->getClusterIds(),
- _keywords,
- std::nullopt,
- Database::Artist::SortMethod::BySortName,
- Database::Range {0, maxEntries}, more)};
-
- if (!artists.empty())
{
- setCondition("if-artists", true);
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
- auto* container {bindNew("artists")};
-
- for (const Database::Artist::pointer& artist : artists)
- container->addWidget(ArtistListHelpers::createEntrySmall(artist));
+ const Database::Range range {results.getCount(), getBatchSize(Mode::Artist)};
+ for (const auto& artist : _artistCollector.get(range, moreResults))
+ results.add(ArtistListHelpers::createEntry(artist));
}
+
+ results.setHasMore(moreResults);
+
+ getItemMenu(Mode::Artist).setDisabled(results.getCount() == 0);
}
void
- SearchView::searchReleases()
+ SearchView::addSomeReleases()
{
- bool more;
- const auto releases {Database::Release::getByFilter(LmsApp->getDbSession(),
- _filters->getClusterIds(),
- _keywords,
- Database::Range {0, maxEntries}, more)};
+ InfiniteScrollingContainer& results {getResultContainer(Mode::Release)};
+ bool moreResults {};
- if (!releases.empty())
{
- setCondition("if-releases", true);
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
- auto* container {bindNew("releases")};
-
- for (const Database::Release::pointer& release : releases)
- container->addWidget(ReleaseListHelpers::createEntry(release));
+ const Database::Range range {results.getCount(), getBatchSize(Mode::Release)};
+ for (const auto& release : _releaseCollector.get(range, moreResults))
+ results.add(ReleaseListHelpers::createEntry(release));
}
+
+ results.setHasMore(moreResults);
+
+ getItemMenu(Mode::Release).setDisabled(results.getCount() == 0);
}
void
- SearchView::searchTracks()
+ SearchView::addSomeTracks()
{
- bool more;
- const auto tracks {Database::Track::getByFilter(LmsApp->getDbSession(),
- _filters->getClusterIds(),
- _keywords,
- Database::Range {0, maxEntries}, more)};
+ InfiniteScrollingContainer& results {getResultContainer(Mode::Track)};
+ bool moreResults {};
- if (!tracks.empty())
{
- setCondition("if-tracks", true);
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
- auto* container {bindNew("tracks")};
-
- for (const Database::Track::pointer& track : tracks)
- container->addWidget(TrackListHelpers::createEntry(track, tracksAction));
+ const Database::Range range {results.getCount(), getBatchSize(Mode::Track)};
+ for (const auto& track : _trackCollector.get(range, moreResults))
+ results.add(TrackListHelpers::createEntry(track, tracksAction));
}
+
+ results.setHasMore(moreResults);
+
+ getItemMenu(Mode::Track).setDisabled(results.getCount() == 0);
}
+
+
} // namespace UserInterface
diff --git a/src/lms/ui/explore/SearchView.hpp b/src/lms/ui/explore/SearchView.hpp
index 5e784be9..0f4b85ab 100644
--- a/src/lms/ui/explore/SearchView.hpp
+++ b/src/lms/ui/explore/SearchView.hpp
@@ -22,34 +22,75 @@
#include
#include
#include
+#include
+#include
+#include
#include
+#include "ArtistCollector.hpp"
+#include "ReleaseCollector.hpp"
+#include "TrackCollector.hpp"
#include "PlayQueueAction.hpp"
-namespace UserInterface {
-
-class Filters;
-
-class SearchView : public Wt::WTemplate
+namespace UserInterface
{
- public:
- SearchView(Filters* filters);
- PlayQueueActionSignal tracksAction;
+ class InfiniteScrollingContainer;
+ class Filters;
- void refreshView(const Wt::WString& searchText);
+ class SearchView : public Wt::WTemplate
+ {
+ public:
+ SearchView(Filters* filters);
- private:
- void refreshView();
- void searchArtists();
- void searchReleases();
- void searchTracks();
+ PlayQueueActionSignal tracksAction;
- Filters* _filters {};
- std::string _searchValue;
- std::vector _keywords;
-};
+ void refreshView(const Wt::WString& searchText);
+
+ private:
+
+ // same order as in the menu
+ enum class Mode
+ {
+ Release,
+ Artist,
+ Track,
+ };
+
+ std::size_t modeToIndex(Mode mode) const;
+ Wt::WMenuItem& getItemMenu(Mode mode) const;
+ InfiniteScrollingContainer& getResultContainer(Mode mode) const;
+
+ static constexpr Mode _defaultMode {Mode::Release};
+ static inline std::unordered_map _batchSizes
+ {
+ {Mode::Artist, 6},
+ {Mode::Release, 6},
+ {Mode::Track, 6},
+ };
+ static inline std::unordered_map _maxCounts
+ {
+ {Mode::Artist, 50},
+ {Mode::Release, 60},
+ {Mode::Track, 50},
+ };
+ std::size_t getBatchSize(Mode mode) const;
+ std::size_t getMaxCount(Mode mode) const;
+
+ void refreshView();
+ void addSomeReleases();
+ void addSomeArtists();
+ void addSomeTracks();
+
+ Filters* _filters {};
+ Wt::WMenu* _menu {};
+ ReleaseCollector _releaseCollector;
+ ArtistCollector _artistCollector;
+ TrackCollector _trackCollector;
+
+ std::vector _results;
+ };
} // namespace UserInterface
diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp
new file mode 100644
index 00000000..6d51d4ea
--- /dev/null
+++ b/src/lms/ui/explore/TrackCollector.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "TrackCollector.hpp"
+
+#include
+
+#include "database/Track.hpp"
+#include "database/User.hpp"
+#include "database/TrackList.hpp"
+#include "scrobbling/IScrobbling.hpp"
+#include "utils/Service.hpp"
+#include "Filters.hpp"
+#include "LmsApplication.hpp"
+
+namespace UserInterface
+{
+ using namespace Database;
+
+ std::vector
+ TrackCollector::get(std::optional range, bool& moreResults)
+ {
+ range = getActualRange(range);
+
+ std::vector releases;
+ switch (getMode())
+ {
+ case Mode::Random:
+ releases = getRandomTracks(range, moreResults);
+ break;
+
+ case Mode::Starred:
+ releases = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case TrackCollector::Mode::RecentlyPlayed:
+ releases = Service::get()->getRecentTracks(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::MostPlayed:
+ releases = Service::get()->getTopTracks(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::RecentlyAdded:
+ releases = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults);
+ break;
+
+ case Mode::Search:
+ releases = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults);
+ break;
+
+ case Mode::All:
+ releases = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults);
+ break;
+ }
+
+ if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount()))
+ moreResults = false;
+
+ return releases;
+ }
+
+ std::vector
+ TrackCollector::getAll()
+ {
+ bool moreResults;
+ const auto releases {get(std::nullopt, moreResults)};
+
+ std::vector res;
+ res.reserve(releases.size());
+ std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Track::pointer& release) { return release.id(); });
+
+ return res;
+ }
+
+ std::vector
+ TrackCollector::getRandomTracks(std::optional range, bool& moreResults)
+ {
+ std::vector releases;
+
+ assert(getMode() == Mode::Random);
+
+ if (_randomTracks.empty())
+ _randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount());
+
+ {
+ auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())};
+ auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())};
+
+ for (auto it {itBegin}; it != itEnd; ++it)
+ {
+ Track::pointer release {Track::getById(LmsApp->getDbSession(), *it)};
+ if (release)
+ releases.push_back(release);
+ }
+
+ moreResults = (itEnd != std::cend(_randomTracks));
+ }
+
+ return releases;
+ }
+
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/TrackCollector.hpp b/src/lms/ui/explore/TrackCollector.hpp
new file mode 100644
index 00000000..11a7c7f7
--- /dev/null
+++ b/src/lms/ui/explore/TrackCollector.hpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "DatabaseCollectorBase.hpp"
+
+namespace Database
+{
+ class Track;
+}
+
+namespace UserInterface
+{
+ class TrackCollector : public DatabaseCollectorBase
+ {
+ public:
+ using DatabaseCollectorBase::DatabaseCollectorBase;
+
+ std::vector> get(std::optional range, bool& moreResults);
+ std::vector getAll();
+ void reset() { _randomTracks.clear(); }
+
+ private:
+ std::vector> getRandomTracks(std::optional range, bool& moreResults);
+ std::vector _randomTracks;
+ };
+} // ns UserInterface
+
diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp
index 683dd563..ba1b30d2 100644
--- a/src/lms/ui/explore/TracksView.cpp
+++ b/src/lms/ui/explore/TracksView.cpp
@@ -19,57 +19,46 @@
#include "TracksView.hpp"
-#include
-#include
#include
#include
#include
-#include "database/Artist.hpp"
-#include "database/Release.hpp"
#include "database/Session.hpp"
-#include "database/Track.hpp"
-#include "database/TrackList.hpp"
-#include "database/User.hpp"
-#include "scrobbling/IScrobbling.hpp"
#include "utils/Logger.hpp"
-#include "utils/String.hpp"
-#include "common/LoadingIndicator.hpp"
+#include "common/InfiniteScrollingContainer.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
-#include "MediaPlayer.hpp"
#include "TrackListHelpers.hpp"
-#include "TrackStringUtils.hpp"
using namespace Database;
namespace UserInterface {
-Tracks::Tracks(Filters* filters)
+Tracks::Tracks(Filters& filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Tracks.template")},
-_filters {filters}
+_trackCollector {filters, _defaultMode, _maxCount}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
{
auto* menu {bindNew("mode")};
- auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
+ auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, TrackCollector::Mode mode)
{
auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); });
- if (mode == defaultMode)
+ if (mode == _defaultMode)
item->renderSelected(true);
};
- addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
- addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
- addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
- addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.random"), TrackCollector::Mode::Random);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), TrackCollector::Mode::Starred);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), TrackCollector::Mode::RecentlyPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), TrackCollector::Mode::MostPlayed);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), TrackCollector::Mode::RecentlyAdded);
+ addItem(*menu, Wt::WString::tr("Lms.Explore.all"), TrackCollector::Mode::All);
}
Wt::WText* playBtn = bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
@@ -97,155 +86,58 @@ _filters {filters}
popup->popup(moreBtn);
});
- _tracksContainer = bindNew("tracks");
- hideLoadingIndicator();
+ _container = bindNew("tracks", Wt::WString::tr("Lms.Explore.Tracks.template.container"));
+ _container->onRequestElements.connect([this]
+ {
+ addSome();
+ });
- filters->updated().connect([this]
+ filters.updated().connect([this]
{
refreshView();
});
- refreshView();
+ refreshView(_trackCollector.getMode());
}
void
Tracks::refreshView()
{
- _tracksContainer->clear();
- _randomTracks.clear();
+ _container->clear();
+ _trackCollector.reset();
addSome();
}
void
-Tracks::refreshView(Mode mode)
+Tracks::refreshView(TrackCollector::Mode mode)
{
- _mode = mode;
+ _trackCollector.setMode(mode);
refreshView();
}
-void
-Tracks::displayLoadingIndicator()
-{
- _loadingIndicator = bindWidget("loading-indicator", createLoadingIndicator());
- _loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
- {
- if (!visible)
- return;
-
- addSome();
- });
-}
-
-void
-Tracks::hideLoadingIndicator()
-{
- _loadingIndicator = nullptr;
- bindEmpty("loading-indicator");
-}
-
-std::vector
-Tracks::getRandomTracks(std::optional range, bool& moreResults)
-{
- std::vector tracks;
-
- if (_randomTracks.empty())
- _randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
-
- {
- auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())};
- auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())};
-
- for (auto it {itBegin}; it != itEnd; ++it)
- {
- const Track::pointer track {Track::getById(LmsApp->getDbSession(), *it)};
- if (track)
- tracks.push_back(track);
- }
-
- moreResults = (itEnd != std::cend(_randomTracks));
- }
-
- return tracks;
-}
-
-std::vector
-Tracks::getTracks(std::optional range, bool& moreResults)
-{
- std::vector tracks;
-
- const std::optional modeLimit{maxItemsPerMode[_mode]};
- if (modeLimit)
- {
- if (range)
- range->limit = std::min(*modeLimit - range->offset, range->limit);
- else
- range = Range {0, *modeLimit};
- }
-
- switch (_mode)
- {
- case Mode::Random:
- tracks = getRandomTracks(range, moreResults);
- break;
-
- case Mode::Starred:
- tracks = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::RecentlyPlayed:
- tracks = Service::get()->getRecentTracks(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::MostPlayed:
- tracks = Service::get()->getTopTracks(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::RecentlyAdded:
- tracks = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
- break;
-
- case Mode::All:
- tracks = Track::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
- break;
- }
-
- if (range && modeLimit && (range->offset + range->limit == *modeLimit))
- moreResults = false;
-
- return tracks;
-}
-
-std::vector
-Tracks::getAllTracks()
-{
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
-
- bool moreResults;
- const auto tracks {getTracks(std::nullopt, moreResults)};
-
- std::vector res;
- res.reserve(tracks.size());
- std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); });
-
- return res;
-}
-
void
Tracks::addSome()
{
- auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+ bool moreResults {};
- bool moreResults;
- for (const Track::pointer& track : getTracks(Range {static_cast(_tracksContainer->count()), batchSize}, moreResults))
{
- _tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction));
+ auto transaction {LmsApp->getDbSession().createSharedTransaction()};
+
+ const auto tracks {_trackCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)};
+
+ for (const auto& track : tracks)
+ _container->add(TrackListHelpers::createEntry(track, tracksAction));
}
- if (moreResults)
- displayLoadingIndicator();
- else
- hideLoadingIndicator();
+ _container->setHasMore(moreResults);
}
+std::vector
+Tracks::getAllTracks()
+{
+ return _trackCollector.getAll();
+}
+
+
} // namespace UserInterface
diff --git a/src/lms/ui/explore/TracksView.hpp b/src/lms/ui/explore/TracksView.hpp
index 13cce32d..62b9bdfa 100644
--- a/src/lms/ui/explore/TracksView.hpp
+++ b/src/lms/ui/explore/TracksView.hpp
@@ -19,7 +19,6 @@
#pragma once
-#include
#include
#include
@@ -27,61 +26,35 @@
#include "database/Types.hpp"
#include "PlayQueueAction.hpp"
+#include "TrackCollector.hpp"
-namespace Database
+namespace UserInterface
{
- class Track;
-}
-namespace UserInterface {
+ class Filters;
+ class InfiniteScrollingContainer;
-class Filters;
-class Tracks : public Wt::WTemplate
-{
- public:
- Tracks(Filters* filters);
+ class Tracks : public Wt::WTemplate
+ {
+ public:
+ Tracks(Filters& filters);
- PlayQueueActionSignal tracksAction;
+ PlayQueueActionSignal tracksAction;
- private:
+ private:
+ void refreshView();
+ void refreshView(TrackCollector::Mode mode);
+ void addSome();
- enum class Mode
- {
- Random,
- Starred,
- RecentlyPlayed,
- RecentlyAdded,
- MostPlayed,
- All
- };
+ std::vector getAllTracks();
- void refreshView();
- void refreshView(Mode mode);
- void displayLoadingIndicator();
- void hideLoadingIndicator();
- void addSome();
+ static constexpr TrackCollector::Mode _defaultMode {TrackCollector::Mode::Random};
+ static constexpr std::size_t _batchSize {6};
+ static constexpr std::size_t _maxCount {128};
- std::vector> getRandomTracks(std::optional range, bool& moreResults);
- std::vector> getTracks(std::optional range, bool& moreResults);
- std::vector getAllTracks();
-
- static constexpr Mode defaultMode {Mode::Random};
- static constexpr std::size_t batchSize {20};
- static inline std::unordered_map> maxItemsPerMode
- {
- {Mode::Random, batchSize * 20},
- {Mode::RecentlyPlayed, batchSize * 10},
- {Mode::RecentlyAdded, batchSize * 10},
- {Mode::MostPlayed, batchSize * 10},
- {Mode::All, batchSize * 50},
- };
-
- Mode _mode {defaultMode};
- Filters* _filters {};
- std::vector _randomTracks;
- Wt::WContainerWidget* _tracksContainer {};
- Wt::WTemplate* _loadingIndicator {};
-};
+ InfiniteScrollingContainer* _container {};
+ TrackCollector _trackCollector;
+ };
} // namespace UserInterface