Refactored the search view. fixes #146

This commit is contained in:
emeric
2021-05-19 19:01:13 +02:00
parent 8929507586
commit d0fc045f63
25 changed files with 1168 additions and 703 deletions
-1
View File
@@ -12,7 +12,6 @@
</div> </div>
</div> </div>
${artists} ${artists}
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</message> </message>
<message id="Lms.Explore.Artists.template.entry"> <message id="Lms.Explore.Artists.template.entry">
+8 -3
View File
@@ -7,10 +7,7 @@
${mode class="nav nav-pills Lms-explore-mode-container"} ${mode class="nav nav-pills Lms-explore-mode-container"}
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} ${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
</div> </div>
<div class="row">
${releases} ${releases}
</div>
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</message> </message>
<message id="Lms.Explore.Releases.template.entry-grid"> <message id="Lms.Explore.Releases.template.entry-grid">
@@ -27,4 +24,12 @@
</div> </div>
</message> </message>
<message id="Lms.Explore.Releases.template.container">
<div class="row">
${elements}
</div>
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</message>
</messages> </messages>
+2 -29
View File
@@ -4,36 +4,9 @@
<message id="Lms.Explore.Search.template"> <message id="Lms.Explore.Search.template">
<div class="Lms-header"> <div class="Lms-header">
<h2>${tr:Lms.Explore.Search.search}</h2> ${mode class="nav nav-pills Lms-explore-mode-container"}
</div>
<div class="row">
${<if-artists>}
<div class="col-lg-8">
<div class="well well-sm">
<h3>${tr:Lms.Explore.artists}</h3>
${artists}
</div>
</div>
${</if-artists>}
${<if-releases>}
<div class="col-lg-8">
<div class="well well-sm">
<h3>${tr:Lms.Explore.releases}</h3>
<div class="row">
${releases}
</div>
</div>
</div>
${</if-releases>}
${<if-tracks>}
<div class="col-lg-8">
<div class="well well-sm">
<h3>${tr:Lms.Explore.tracks}</h3>
${tracks}
</div>
</div>
${</if-tracks>}
</div> </div>
${stack}
</message> </message>
</messages> </messages>
+6 -1
View File
@@ -18,8 +18,8 @@
</div> </div>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li>${artists class="Lms-btn"}</li>
<li>${releases class="Lms-btn"}</li> <li>${releases class="Lms-btn"}</li>
<li>${artists class="Lms-btn"}</li>
<li>${tracks class="Lms-btn"}</li> <li>${tracks class="Lms-btn"}</li>
<li>${playqueue class="Lms-btn"}</li> <li>${playqueue class="Lms-btn"}</li>
</ul> </ul>
@@ -61,4 +61,9 @@
<span class="sr-only">${tr:Lms.loading}</span> <span class="sr-only">${tr:Lms.loading}</span>
</message> </message>
<message id="Lms.infinite-scrolling-container">
${elements}
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</message>
</messages> </messages>
+9 -5
View File
@@ -4,16 +4,11 @@
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Lms.Explore.Tracks.template"> <message id="Lms.Explore.Tracks.template">
<div class="row">
<div class="col-lg-8">
<div class="Lms-header"> <div class="Lms-header">
${mode class="nav nav-pills Lms-explore-mode-container"} ${mode class="nav nav-pills Lms-explore-mode-container"}
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"} ${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
</div> </div>
${tracks} ${tracks}
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</div>
</div>
</message> </message>
<message id="Lms.Explore.Tracks.template.entry"> <message id="Lms.Explore.Tracks.template.entry">
@@ -45,4 +40,13 @@
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small> <small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
</message> </message>
<message id="Lms.Explore.Tracks.template.container">
<div class="row">
<div class="col-lg-8">
${elements}
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
</div>
</div>
</message>
</messages> </messages>
+5
View File
@@ -15,21 +15,26 @@ add_executable(lms
ui/admin/UserView.cpp ui/admin/UserView.cpp
ui/admin/UsersView.cpp ui/admin/UsersView.cpp
ui/common/DirectoryValidator.cpp ui/common/DirectoryValidator.cpp
ui/common/InfiniteScrollingContainer.cpp
ui/common/LoadingIndicator.cpp ui/common/LoadingIndicator.cpp
ui/common/LoginNameValidator.cpp ui/common/LoginNameValidator.cpp
ui/common/MandatoryValidator.cpp ui/common/MandatoryValidator.cpp
ui/common/PasswordValidator.cpp ui/common/PasswordValidator.cpp
ui/common/UUIDValidator.cpp ui/common/UUIDValidator.cpp
ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp ui/explore/ArtistListHelpers.cpp
ui/explore/ArtistView.cpp ui/explore/ArtistView.cpp
ui/explore/ArtistsView.cpp ui/explore/ArtistsView.cpp
ui/explore/DatabaseCollectorBase.cpp
ui/explore/Explore.cpp ui/explore/Explore.cpp
ui/explore/Filters.cpp ui/explore/Filters.cpp
ui/explore/ReleaseCollector.cpp
ui/explore/ReleaseListHelpers.cpp ui/explore/ReleaseListHelpers.cpp
ui/explore/ReleasePopup.cpp ui/explore/ReleasePopup.cpp
ui/explore/ReleasesView.cpp ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp ui/explore/ReleaseView.cpp
ui/explore/SearchView.cpp ui/explore/SearchView.cpp
ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp ui/explore/TrackListHelpers.cpp
ui/explore/TrackPopup.cpp ui/explore/TrackPopup.cpp
ui/explore/TracksView.cpp ui/explore/TracksView.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 <http://www.gnu.org/licenses/>.
*/
#include "InfiniteScrollingContainer.hpp"
#include "LoadingIndicator.hpp"
namespace UserInterface
{
InfiniteScrollingContainer::InfiniteScrollingContainer(const Wt::WString& text)
: Wt::WTemplate {text}
, _elements {bindNew<Wt::WContainerWidget>("elements")}
, _loadingIndicator {bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator())}
{
hideLoadingIndicator();
}
void
InfiniteScrollingContainer::clear()
{
_elements->clear();
hideLoadingIndicator();
}
std::size_t
InfiniteScrollingContainer::getCount()
{
return _elements->count();
}
void
InfiniteScrollingContainer::add(std::unique_ptr<Wt::WWidget> result)
{
return _elements->addWidget(std::move(result));
}
void
InfiniteScrollingContainer::setHasMore(bool hasMore)
{
if (hasMore)
displayLoadingIndicator();
else
hideLoadingIndicator();
}
void
InfiniteScrollingContainer::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
onRequestElements.emit();
});
}
void
InfiniteScrollingContainer::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
#include <Wt/WSignal.h>
#include <Wt/WString.h>
#include <Wt/WTemplate.h>
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<Wt::WWidget> result);
void setHasMore(bool hasMore);
Wt::Signal<> onRequestElements;
private:
void displayLoadingIndicator();
void hideLoadingIndicator();
Wt::WContainerWidget* _elements;
Wt::WTemplate* _loadingIndicator;
};
}
+130
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Wt::Dbo::ptr<Database::Artist>>
ArtistCollector::get(std::optional<Database::Range> range, bool& moreResults)
{
range = getActualRange(range);
std::vector<Artist::pointer> 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<Scrobbling::IScrobbling>::get()->getRecentArtists(LmsApp->getDbSession(), LmsApp->getUser(),
getFilters().getClusterIds(),
_linkType,
range, moreResults);
break;
case Mode::MostPlayed:
artists = Service<Scrobbling::IScrobbling>::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<Database::Artist::pointer>
ArtistCollector::getRandomArtists(std::optional<Range> range, bool& moreResults)
{
std::vector<Artist::pointer> 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
+51
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <vector>
#include "DatabaseCollectorBase.hpp"
#include "database/Types.hpp"
namespace Database
{
class Artist;
}
namespace UserInterface
{
class ArtistCollector : public DatabaseCollectorBase
{
public:
using DatabaseCollectorBase::DatabaseCollectorBase;
std::vector<Wt::Dbo::ptr<Database::Artist>> get(std::optional<Database::Range> range, bool& moreResults);
void reset() { _randomArtists.clear(); }
void setArtistLinkType(std::optional<Database::TrackArtistLinkType> linkType) { _linkType = linkType; }
private:
std::vector<Wt::Dbo::ptr<Database::Artist>> getRandomArtists(std::optional<Range> range, bool& moreResults);
std::vector<Database::IdType> _randomArtists;
std::optional<Database::TrackArtistLinkType> _linkType;
};
} // ns UserInterface
+49 -155
View File
@@ -19,23 +19,18 @@
#include "ArtistsView.hpp" #include "ArtistsView.hpp"
#include <optional>
#include <Wt/WMenu.h> #include <Wt/WMenu.h>
#include "common/ValueStringModel.hpp"
#include "database/Artist.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp"
#include "database/TrackArtistLink.hpp" #include "database/TrackArtistLink.hpp"
#include "database/TrackList.hpp" #include "utils/EnumSet.hpp"
#include "scrobbling/IScrobbling.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "common/LoadingIndicator.hpp" #include "common/ValueStringModel.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "ArtistListHelpers.hpp" #include "ArtistListHelpers.hpp"
#include "LmsApplication.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp"
using namespace Database; using namespace Database;
@@ -43,35 +38,39 @@ namespace UserInterface {
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>; using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
Artists::Artists(Filters* filters) Artists::Artists(Filters& filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
_filters {filters} , _artistCollector {filters, _defaultMode, _maxCount}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
{ {
auto* menu {bindNew<Wt::WMenu>("mode")}; auto* menu {bindNew<Wt::WMenu>("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)}; auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); }); item->clicked().connect([this, mode] { refreshView(mode); });
if (mode == defaultMode) if (mode == _defaultMode)
item->renderSelected(true); item->renderSelected(true);
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), ArtistCollector::Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred); addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), ArtistCollector::Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), ArtistCollector::Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), ArtistCollector::Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), ArtistCollector::Mode::RecentlyAdded);
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All); addItem(*menu, Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
} }
_linkType = bindNew<Wt::WComboBox>("link-type"); _linkType = bindNew<Wt::WComboBox>("link-type");
_linkType->setModel(std::make_shared<ArtistLinkModel>()); _linkType->setModel(std::make_shared<ArtistLinkModel>());
_linkType->changed().connect([this] { refreshView(); }); _linkType->changed().connect([this]
{
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
refreshView(linkType);
});
refreshArtistLinkTypes(); refreshArtistLinkTypes();
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
@@ -80,26 +79,39 @@ Artists::Artists(Filters* filters)
refreshArtistLinkTypes(); refreshArtistLinkTypes();
}); });
_container = bindNew<Wt::WContainerWidget>("artists"); _container = bindNew<InfiniteScrollingContainer>("artists");
hideLoadingIndicator(); _container->onRequestElements.connect([this]
{
addSome();
});
filters.updated().connect([this]
{
refreshView(); refreshView();
});
filters->updated().connect([this] { refreshView(); }); refreshView(_artistCollector.getMode());
} }
void void
Artists::refreshView() Artists::refreshView()
{ {
_container->clear(); _container->clear();
_randomArtists.clear(); _artistCollector.reset();
addSome(); addSome();
} }
void void
Artists::refreshView(Mode mode) Artists::refreshView(ArtistCollector::Mode mode)
{ {
_mode = mode; _artistCollector.setMode(mode);
refreshView();
}
void
Artists::refreshView(std::optional<Database::TrackArtistLinkType> linkType)
{
_artistCollector.setArtistLinkType(linkType);
refreshView(); refreshView();
} }
@@ -124,7 +136,9 @@ Artists::refreshArtistLinkTypes()
linkTypeModel->clear(); 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::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist"); addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -134,140 +148,20 @@ Artists::refreshArtistLinkTypes()
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
} }
void
Artists::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Artists::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
std::vector<Artist::pointer>
Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
{
std::vector<Artist::pointer> artists;
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_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<Artist::pointer>
Artists::getArtists(std::optional<Range> range, bool& moreResults)
{
std::vector<Artist::pointer> artists;
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
const std::optional<std::size_t> 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<Scrobbling::IScrobbling>::get()->getRecentArtists(LmsApp->getDbSession(), LmsApp->getUser(),
_filters->getClusterIds(),
linkType,
range, moreResults);
break;
case Mode::MostPlayed:
artists = Service<Scrobbling::IScrobbling>::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 void
Artists::addSome() Artists::addSome()
{ {
bool moreResults {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
bool moreResults {}; const auto artists {_artistCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}, moreResults)};
for (const Artist::pointer& artist : getArtists(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults)) for (const auto& artist : artists)
{ _container->add(ArtistListHelpers::createEntry(artist));
_container->addWidget(ArtistListHelpers::createEntry(artist));
} }
if (moreResults) _container->setHasMore(moreResults);
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
} // namespace UserInterface } // namespace UserInterface
+16 -46
View File
@@ -19,69 +19,39 @@
#pragma once #pragma once
#include <memory> #include <optional>
#include <unordered_map> #include <unordered_map>
#include <vector>
#include <Wt/WComboBox.h> #include <Wt/WComboBox.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "ArtistCollector.hpp"
namespace Database namespace UserInterface
{ {
class Artist; class Filters;
} class InfiniteScrollingContainer;
namespace UserInterface { class Artists : public Wt::WTemplate
{
class Filters;
class Artists : public Wt::WTemplate
{
public: public:
Artists(Filters* filters); Artists(Filters& filters);
private: private:
enum class Mode
{
Random,
Starred,
RecentlyPlayed,
RecentlyAdded,
MostPlayed,
All
};
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(ArtistCollector::Mode mode);
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
void refreshArtistLinkTypes(); void refreshArtistLinkTypes();
void displayLoadingIndicator();
void hideLoadingIndicator();
void addSome(); void addSome();
std::vector<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults); static constexpr std::size_t _batchSize {30};
std::vector<Wt::Dbo::ptr<Database::Artist>> getRandomArtists(std::optional<Database::Range> range, bool& moreResults); static constexpr std::size_t _maxCount {128};
static constexpr Mode defaultMode {Mode::Random}; InfiniteScrollingContainer* _container {};
static constexpr std::size_t batchSize {30}; ArtistCollector _artistCollector;
static inline std::unordered_map<Mode, std::optional<std::size_t>> 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<Database::IdType> _randomArtists;
Filters* _filters {};
Wt::WTemplate* _loadingIndicator {};
Wt::WContainerWidget* _container {};
Wt::WComboBox* _linkType {}; Wt::WComboBox* _linkType {};
}; static constexpr ArtistCollector::Mode _defaultMode {ArtistCollector::Mode::Random};
};
} // namespace UserInterface } // namespace UserInterface
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "DatabaseCollectorBase.hpp"
#include "utils/String.hpp"
namespace UserInterface
{
DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional<std::size_t> maxCount)
: _filters {filters}
, _mode {defaultMode}
, _maxCount {maxCount}
{
}
std::optional<DatabaseCollectorBase::Range>
DatabaseCollectorBase::getActualRange(std::optional<Range> range) const
{
if (std::optional<std::size_t> maxCount {getMaxCount()})
{
if (range)
range->limit = std::min(*maxCount - range->offset, range->limit);
else
range = Range {0, *maxCount};
}
return range;
}
std::optional<std::size_t>
DatabaseCollectorBase::getMaxCount() const
{
return _maxCount;
}
void
DatabaseCollectorBase::setSearch(std::string_view searchText)
{
_searchText = searchText;
_searchKeywords = StringUtils::splitString(_searchText, " ");
}
} // ns UserInterface
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#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<std::size_t> 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<Range> getActualRange(std::optional<Range> range) const;
std::optional<std::size_t> getMaxCount() const;
Filters& getFilters() { return _filters; }
const std::vector<std::string_view>& getSearchKeywords() const { return _searchKeywords; }
private:
Filters& _filters;
std::string _searchText;
std::vector<std::string_view> _searchKeywords;
Mode _mode;
std::optional<std::size_t> _maxCount;
};
} // ns UserInterface
+3 -3
View File
@@ -88,14 +88,14 @@ Explore::Explore(Filters* filters)
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents"); Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;"); contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
auto artists = std::make_unique<Artists>(_filters); auto artists = std::make_unique<Artists>(*_filters);
contentsStack->addWidget(std::move(artists)); contentsStack->addWidget(std::move(artists));
auto artist = std::make_unique<Artist>(_filters); auto artist = std::make_unique<Artist>(_filters);
artist->artistsAction.connect(this, &Explore::handleArtistsAction); artist->artistsAction.connect(this, &Explore::handleArtistsAction);
contentsStack->addWidget(std::move(artist)); contentsStack->addWidget(std::move(artist));
auto releases = std::make_unique<Releases>(_filters); auto releases = std::make_unique<Releases>(*_filters);
releases->releasesAction.connect(this, &Explore::handleReleasesAction); releases->releasesAction.connect(this, &Explore::handleReleasesAction);
contentsStack->addWidget(std::move(releases)); contentsStack->addWidget(std::move(releases));
@@ -109,7 +109,7 @@ Explore::Explore(Filters* filters)
_search = search.get(); _search = search.get();
contentsStack->addWidget(std::move(search)); contentsStack->addWidget(std::move(search));
auto tracks = std::make_unique<Tracks>(_filters); auto tracks = std::make_unique<Tracks>(*_filters);
tracks->tracksAction.connect(this, &Explore::handleTracksAction); tracks->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(tracks)); contentsStack->addWidget(std::move(tracks));
+120
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ReleaseCollector.hpp"
#include <algorithm>
#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<Release::pointer>
ReleaseCollector::get(std::optional<Database::Range> range, bool& moreResults)
{
range = getActualRange(range);
std::vector<Release::pointer> 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<Scrobbling::IScrobbling>::get()->getRecentReleases(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
break;
case Mode::MostPlayed:
releases = Service<Scrobbling::IScrobbling>::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<Database::IdType>
ReleaseCollector::getAll()
{
bool moreResults;
const auto releases {get(std::nullopt, moreResults)};
std::vector<IdType> 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<Database::Release::pointer>
ReleaseCollector::getRandomReleases(std::optional<Range> range, bool& moreResults)
{
std::vector<Release::pointer> 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
+47
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vector>
#include "DatabaseCollectorBase.hpp"
namespace Database
{
class Release;
}
namespace UserInterface
{
class ReleaseCollector : public DatabaseCollectorBase
{
public:
using DatabaseCollectorBase::DatabaseCollectorBase;
std::vector<Wt::Dbo::ptr<Database::Release>> get(std::optional<Database::Range> range, bool& moreResults);
std::vector<Database::IdType> getAll();
void reset() { _randomReleases.clear(); }
private:
std::vector<Wt::Dbo::ptr<Database::Release>> getRandomReleases(std::optional<Range> range, bool& moreResults);
std::vector<Database::IdType> _randomReleases;
};
} // ns UserInterface
+31 -140
View File
@@ -19,23 +19,12 @@
#include "ReleasesView.hpp" #include "ReleasesView.hpp"
#include <algorithm>
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WMenu.h> #include <Wt/WMenu.h>
#include <Wt/WPopupMenu.h> #include <Wt/WPopupMenu.h>
#include <Wt/WText.h> #include <Wt/WText.h>
#include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/TrackList.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "database/User.hpp"
#include "scrobbling/IScrobbling.hpp"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
#include "common/LoadingIndicator.hpp"
#include "ReleaseListHelpers.hpp" #include "ReleaseListHelpers.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -44,30 +33,30 @@ using namespace Database;
namespace UserInterface { namespace UserInterface {
Releases::Releases(Filters* filters) Releases::Releases(Filters& filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")}
_filters {filters} , _releaseCollector {filters, _defaultMode, _maxCount}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
{ {
auto* menu {bindNew<Wt::WMenu>("mode")}; auto* menu {bindNew<Wt::WMenu>("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)}; auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); }); item->clicked().connect([this, mode] { refreshView(mode); });
if (mode == defaultMode) if (mode == _defaultMode)
item->renderSelected(true); item->renderSelected(true);
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), ReleaseCollector::Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred); addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), ReleaseCollector::Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), ReleaseCollector::Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), ReleaseCollector::Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), ReleaseCollector::Mode::RecentlyAdded);
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All); addItem(*menu, Wt::WString::tr("Lms.Explore.all"), ReleaseCollector::Mode::All);
} }
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
@@ -94,153 +83,55 @@ _filters {filters}
popup->popup(moreBtn); popup->popup(moreBtn);
}); });
_container = bindNew<Wt::WContainerWidget>("releases"); _container = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
hideLoadingIndicator(); _container->onRequestElements.connect([this]
{
addSome();
});
refreshView(defaultMode); filters.updated().connect([this]
{
refreshView();
});
filters->updated().connect([this] { refreshView(); }); refreshView(_releaseCollector.getMode());
} }
void void
Releases::refreshView() Releases::refreshView()
{ {
_container->clear(); _container->clear();
_randomReleases.clear(); _releaseCollector.reset();
addSome(); addSome();
} }
void void
Releases::refreshView(Mode mode) Releases::refreshView(ReleaseCollector::Mode mode)
{ {
_mode = mode; _releaseCollector.setMode(mode);
refreshView(); refreshView();
} }
void
Releases::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Releases::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
void void
Releases::addSome() Releases::addSome()
{ {
bool moreResults {}; bool moreResults {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto releases {getReleases(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults)};
for (const Release::pointer& release : releases) const auto releases {_releaseCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}, moreResults)};
{ for (const auto& release : releases)
_container->addWidget(ReleaseListHelpers::createEntry(release)); _container->add(ReleaseListHelpers::createEntry(release));
} }
if (moreResults) _container->setHasMore(moreResults);
displayLoadingIndicator();
else
hideLoadingIndicator();
}
std::vector<Database::Release::pointer>
Releases::getRandomReleases(std::optional<Range> range, bool& moreResults)
{
std::vector<Release::pointer> 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<Database::Release::pointer>
Releases::getReleases(std::optional<Range> range, bool& moreResults)
{
std::vector<Release::pointer> releases;
const std::optional<std::size_t> 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<Scrobbling::IScrobbling>::get()->getRecentReleases(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
break;
case Mode::MostPlayed:
releases = Service<Scrobbling::IScrobbling>::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;
} }
std::vector<Database::IdType> std::vector<Database::IdType>
Releases::getAllReleases() Releases::getAllReleases()
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; return _releaseCollector.getAll();
bool moreResults;
const auto releases {getReleases(std::nullopt, moreResults)};
std::vector<IdType> 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;
} }
} // namespace UserInterface } // namespace UserInterface
+15 -44
View File
@@ -19,7 +19,6 @@
#pragma once #pragma once
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
@@ -27,63 +26,35 @@
#include "database/Types.hpp" #include "database/Types.hpp"
#include "PlayQueueAction.hpp" #include "PlayQueueAction.hpp"
#include "ReleaseCollector.hpp"
namespace Database namespace UserInterface
{ {
class Release; class Filters;
} class InfiniteScrollingContainer;
namespace UserInterface { class Releases : public Wt::WTemplate
{
class Filters;
class Releases : public Wt::WTemplate
{
public: public:
Releases(Filters* filters); Releases(Filters& filters);
PlayQueueActionSignal releasesAction; PlayQueueActionSignal releasesAction;
private: private:
enum class Mode
{
Random,
Starred,
RecentlyPlayed,
RecentlyAdded,
MostPlayed,
All
};
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(ReleaseCollector::Mode mode);
void displayLoadingIndicator();
void hideLoadingIndicator();
void addSome(); void addSome();
std::vector<Wt::Dbo::ptr<Database::Release>> getReleases(std::optional<Database::Range> range, bool& moreResults);
std::vector<Wt::Dbo::ptr<Database::Release>> getRandomReleases(std::optional<Database::Range> range, bool& moreResults);
std::vector<Database::IdType> getAllReleases(); std::vector<Database::IdType> getAllReleases();
static constexpr Mode defaultMode {Mode::Random}; static constexpr std::size_t _maxItemsPerLine {6};
static constexpr std::size_t maxItemsPerLine {6}; static constexpr std::size_t _batchSize {_maxItemsPerLine};
static constexpr std::size_t batchSize {maxItemsPerLine * 3}; static constexpr std::size_t _maxCount {_maxItemsPerLine * 24};
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
{ InfiniteScrollingContainer* _container {};
{Mode::Random, batchSize * 10}, ReleaseCollector _releaseCollector;
{Mode::RecentlyPlayed, batchSize * 10}, static constexpr ReleaseCollector::Mode _defaultMode {ReleaseCollector::Mode::Random};
{Mode::RecentlyAdded, batchSize * 10},
{Mode::MostPlayed, batchSize * 10},
{Mode::All, batchSize * 30},
}; };
Mode _mode {defaultMode};
Filters* _filters {};
std::vector<Database::IdType> _randomReleases;
Wt::WContainerWidget* _container {};
Wt::WTemplate* _loadingIndicator {};
};
} // namespace UserInterface } // namespace UserInterface
+111 -51
View File
@@ -19,14 +19,19 @@
#include "SearchView.hpp" #include "SearchView.hpp"
#include <functional>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WStackedWidget.h>
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "common/LoadingIndicator.hpp"
#include "ArtistListHelpers.hpp" #include "ArtistListHelpers.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -41,97 +46,152 @@ namespace UserInterface
SearchView::SearchView(Filters* filters) SearchView::SearchView(Filters* filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")} : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
, _filters {filters} , _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); addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WStackedWidget* stack {bindNew<Wt::WStackedWidget>("stack")};
stack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
_menu = bindNew<Wt::WMenu>("mode", stack);
auto addItem = [=](const Wt::WString& str, Mode mode, const Wt::WString& templateStr, std::function<void()> onRequestElementsFunc)
{
assert(modeToIndex(mode) == _results.size());
auto results {std::make_unique<InfiniteScrollingContainer>(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([=] _filters->updated().connect([=]
{ {
refreshView(); refreshView();
}); });
refreshView();
}
std::size_t
SearchView::modeToIndex(Mode mode) const
{
return static_cast<std::size_t>(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 void
SearchView::refreshView(const Wt::WString& searchText) SearchView::refreshView(const Wt::WString& searchText)
{ {
_searchValue = searchText.toUTF8(); _releaseCollector.setSearch(searchText.toUTF8());
_keywords = StringUtils::splitString(_searchValue, " "); _artistCollector.setSearch(searchText.toUTF8());
_trackCollector.setSearch(searchText.toUTF8());
refreshView(); refreshView();
} }
void void
SearchView::refreshView() SearchView::refreshView()
{ {
clear(); for (InfiniteScrollingContainer* results : _results)
results->clear();
addSomeReleases();
addSomeArtists();
addSomeTracks();
}
void
SearchView::addSomeArtists()
{
InfiniteScrollingContainer& results {getResultContainer(Mode::Artist)};
bool moreResults {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
searchArtists(); const Database::Range range {results.getCount(), getBatchSize(Mode::Artist)};
searchReleases(); for (const auto& artist : _artistCollector.get(range, moreResults))
searchTracks(); results.add(ArtistListHelpers::createEntry(artist));
}
results.setHasMore(moreResults);
getItemMenu(Mode::Artist).setDisabled(results.getCount() == 0);
} }
void void
SearchView::searchArtists() SearchView::addSomeReleases()
{ {
bool more; InfiniteScrollingContainer& results {getResultContainer(Mode::Release)};
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<Wt::WContainerWidget>("artists")}; const Database::Range range {results.getCount(), getBatchSize(Mode::Release)};
for (const auto& release : _releaseCollector.get(range, moreResults))
for (const Database::Artist::pointer& artist : artists) results.add(ReleaseListHelpers::createEntry(release));
container->addWidget(ArtistListHelpers::createEntrySmall(artist));
} }
results.setHasMore(moreResults);
getItemMenu(Mode::Release).setDisabled(results.getCount() == 0);
} }
void void
SearchView::searchReleases() SearchView::addSomeTracks()
{ {
bool more; InfiniteScrollingContainer& results {getResultContainer(Mode::Track)};
const auto releases {Database::Release::getByFilter(LmsApp->getDbSession(), bool moreResults {};
_filters->getClusterIds(),
_keywords,
Database::Range {0, maxEntries}, more)};
if (!releases.empty())
{ {
setCondition("if-releases", true); auto transaction {LmsApp->getDbSession().createSharedTransaction()};
auto* container {bindNew<Wt::WContainerWidget>("releases")}; const Database::Range range {results.getCount(), getBatchSize(Mode::Track)};
for (const auto& track : _trackCollector.get(range, moreResults))
for (const Database::Release::pointer& release : releases) results.add(TrackListHelpers::createEntry(track, tracksAction));
container->addWidget(ReleaseListHelpers::createEntry(release));
}
} }
void results.setHasMore(moreResults);
SearchView::searchTracks()
{
bool more;
const auto tracks {Database::Track::getByFilter(LmsApp->getDbSession(),
_filters->getClusterIds(),
_keywords,
Database::Range {0, maxEntries}, more)};
if (!tracks.empty()) getItemMenu(Mode::Track).setDisabled(results.getCount() == 0);
{
setCondition("if-tracks", true);
auto* container {bindNew<Wt::WContainerWidget>("tracks")};
for (const Database::Track::pointer& track : tracks)
container->addWidget(TrackListHelpers::createEntry(track, tracksAction));
}
} }
} // namespace UserInterface } // namespace UserInterface
+52 -11
View File
@@ -22,17 +22,25 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
#include <unordered_map>
#include <Wt/WContainerWidget.h>
#include <Wt/WMenu.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "ArtistCollector.hpp"
#include "ReleaseCollector.hpp"
#include "TrackCollector.hpp"
#include "PlayQueueAction.hpp" #include "PlayQueueAction.hpp"
namespace UserInterface { namespace UserInterface
class Filters;
class SearchView : public Wt::WTemplate
{ {
class InfiniteScrollingContainer;
class Filters;
class SearchView : public Wt::WTemplate
{
public: public:
SearchView(Filters* filters); SearchView(Filters* filters);
@@ -41,15 +49,48 @@ class SearchView : public Wt::WTemplate
void refreshView(const Wt::WString& searchText); void refreshView(const Wt::WString& searchText);
private: 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<Mode, std::size_t> _batchSizes
{
{Mode::Artist, 6},
{Mode::Release, 6},
{Mode::Track, 6},
};
static inline std::unordered_map<Mode, std::size_t> _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 refreshView();
void searchArtists(); void addSomeReleases();
void searchReleases(); void addSomeArtists();
void searchTracks(); void addSomeTracks();
Filters* _filters {}; Filters* _filters {};
std::string _searchValue; Wt::WMenu* _menu {};
std::vector<std::string_view> _keywords; ReleaseCollector _releaseCollector;
}; ArtistCollector _artistCollector;
TrackCollector _trackCollector;
std::vector<InfiniteScrollingContainer*> _results;
};
} // namespace UserInterface } // namespace UserInterface
+120
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "TrackCollector.hpp"
#include <algorithm>
#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<Track::pointer>
TrackCollector::get(std::optional<Database::Range> range, bool& moreResults)
{
range = getActualRange(range);
std::vector<Track::pointer> 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<Scrobbling::IScrobbling>::get()->getRecentTracks(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults);
break;
case Mode::MostPlayed:
releases = Service<Scrobbling::IScrobbling>::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<Database::IdType>
TrackCollector::getAll()
{
bool moreResults;
const auto releases {get(std::nullopt, moreResults)};
std::vector<IdType> 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<Database::Track::pointer>
TrackCollector::getRandomTracks(std::optional<Range> range, bool& moreResults)
{
std::vector<Track::pointer> 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
+47
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vector>
#include "DatabaseCollectorBase.hpp"
namespace Database
{
class Track;
}
namespace UserInterface
{
class TrackCollector : public DatabaseCollectorBase
{
public:
using DatabaseCollectorBase::DatabaseCollectorBase;
std::vector<Wt::Dbo::ptr<Database::Track>> get(std::optional<Database::Range> range, bool& moreResults);
std::vector<Database::IdType> getAll();
void reset() { _randomTracks.clear(); }
private:
std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Range> range, bool& moreResults);
std::vector<Database::IdType> _randomTracks;
};
} // ns UserInterface
+37 -145
View File
@@ -19,57 +19,46 @@
#include "TracksView.hpp" #include "TracksView.hpp"
#include <Wt/WAnchor.h>
#include <Wt/WLineEdit.h>
#include <Wt/WMenu.h> #include <Wt/WMenu.h>
#include <Wt/WPopupMenu.h> #include <Wt/WPopupMenu.h>
#include <Wt/WText.h> #include <Wt/WText.h>
#include "database/Artist.hpp"
#include "database/Release.hpp"
#include "database/Session.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/Logger.hpp"
#include "utils/String.hpp"
#include "common/LoadingIndicator.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackListHelpers.hpp" #include "TrackListHelpers.hpp"
#include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
namespace UserInterface { namespace UserInterface {
Tracks::Tracks(Filters* filters) Tracks::Tracks(Filters& filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Tracks.template")}, : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Tracks.template")},
_filters {filters} _trackCollector {filters, _defaultMode, _maxCount}
{ {
addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("tr", &Wt::WTemplate::Functions::tr);
{ {
auto* menu {bindNew<Wt::WMenu>("mode")}; auto* menu {bindNew<Wt::WMenu>("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)}; auto* item {menu.addItem(str)};
item->clicked().connect([this, mode] { refreshView(mode); }); item->clicked().connect([this, mode] { refreshView(mode); });
if (mode == defaultMode) if (mode == _defaultMode)
item->renderSelected(true); item->renderSelected(true);
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), TrackCollector::Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred); addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), TrackCollector::Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), TrackCollector::Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), TrackCollector::Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), TrackCollector::Mode::RecentlyAdded);
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All); addItem(*menu, Wt::WString::tr("Lms.Explore.all"), TrackCollector::Mode::All);
} }
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
@@ -97,155 +86,58 @@ _filters {filters}
popup->popup(moreBtn); popup->popup(moreBtn);
}); });
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks"); _container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.Tracks.template.container"));
hideLoadingIndicator(); _container->onRequestElements.connect([this]
{
addSome();
});
filters->updated().connect([this] filters.updated().connect([this]
{ {
refreshView(); refreshView();
}); });
refreshView(); refreshView(_trackCollector.getMode());
} }
void void
Tracks::refreshView() Tracks::refreshView()
{ {
_tracksContainer->clear(); _container->clear();
_randomTracks.clear(); _trackCollector.reset();
addSome(); addSome();
} }
void void
Tracks::refreshView(Mode mode) Tracks::refreshView(TrackCollector::Mode mode)
{ {
_mode = mode; _trackCollector.setMode(mode);
refreshView(); refreshView();
} }
void
Tracks::displayLoadingIndicator()
{
_loadingIndicator = bindWidget<Wt::WTemplate>("loading-indicator", createLoadingIndicator());
_loadingIndicator->scrollVisibilityChanged().connect([this](bool visible)
{
if (!visible)
return;
addSome();
});
}
void
Tracks::hideLoadingIndicator()
{
_loadingIndicator = nullptr;
bindEmpty("loading-indicator");
}
std::vector<Database::Track::pointer>
Tracks::getRandomTracks(std::optional<Range> range, bool& moreResults)
{
std::vector<Track::pointer> 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<Track::pointer>
Tracks::getTracks(std::optional<Range> range, bool& moreResults)
{
std::vector<Track::pointer> tracks;
const std::optional<std::size_t> 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<Scrobbling::IScrobbling>::get()->getRecentTracks(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
break;
case Mode::MostPlayed:
tracks = Service<Scrobbling::IScrobbling>::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<Database::IdType>
Tracks::getAllTracks()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
bool moreResults;
const auto tracks {getTracks(std::nullopt, moreResults)};
std::vector<Database::IdType> 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 void
Tracks::addSome() Tracks::addSome()
{ {
bool moreResults {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; auto transaction {LmsApp->getDbSession().createSharedTransaction()};
bool moreResults; const auto tracks {_trackCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}, moreResults)};
for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
{ for (const auto& track : tracks)
_tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction)); _container->add(TrackListHelpers::createEntry(track, tracksAction));
} }
if (moreResults) _container->setHasMore(moreResults);
displayLoadingIndicator();
else
hideLoadingIndicator();
} }
std::vector<Database::IdType>
Tracks::getAllTracks()
{
return _trackCollector.getAll();
}
} // namespace UserInterface } // namespace UserInterface
+14 -41
View File
@@ -19,7 +19,6 @@
#pragma once #pragma once
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
@@ -27,61 +26,35 @@
#include "database/Types.hpp" #include "database/Types.hpp"
#include "PlayQueueAction.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
class Tracks : public Wt::WTemplate {
{
public: public:
Tracks(Filters* filters); Tracks(Filters& filters);
PlayQueueActionSignal tracksAction; PlayQueueActionSignal tracksAction;
private: private:
enum class Mode
{
Random,
Starred,
RecentlyPlayed,
RecentlyAdded,
MostPlayed,
All
};
void refreshView(); void refreshView();
void refreshView(Mode mode); void refreshView(TrackCollector::Mode mode);
void displayLoadingIndicator();
void hideLoadingIndicator();
void addSome(); void addSome();
std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Database::Range> range, bool& moreResults);
std::vector<Wt::Dbo::ptr<Database::Track>> getTracks(std::optional<Database::Range> range, bool& moreResults);
std::vector<Database::IdType> getAllTracks(); std::vector<Database::IdType> getAllTracks();
static constexpr Mode defaultMode {Mode::Random}; static constexpr TrackCollector::Mode _defaultMode {TrackCollector::Mode::Random};
static constexpr std::size_t batchSize {20}; static constexpr std::size_t _batchSize {6};
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode static constexpr std::size_t _maxCount {128};
{
{Mode::Random, batchSize * 20},
{Mode::RecentlyPlayed, batchSize * 10},
{Mode::RecentlyAdded, batchSize * 10},
{Mode::MostPlayed, batchSize * 10},
{Mode::All, batchSize * 50},
};
Mode _mode {defaultMode}; InfiniteScrollingContainer* _container {};
Filters* _filters {}; TrackCollector _trackCollector;
std::vector<Database::IdType> _randomTracks; };
Wt::WContainerWidget* _tracksContainer {};
Wt::WTemplate* _loadingIndicator {};
};
} // namespace UserInterface } // namespace UserInterface