Refactored the search view. fixes #146
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,23 +19,18 @@
|
||||
|
||||
#include "ArtistsView.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WMenu.h>
|
||||
|
||||
#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<std::optional<TrackArtistLinkType>>;
|
||||
|
||||
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<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)};
|
||||
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<Wt::WComboBox>("link-type");
|
||||
_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();
|
||||
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||
@@ -80,26 +79,39 @@ Artists::Artists(Filters* filters)
|
||||
refreshArtistLinkTypes();
|
||||
});
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("artists");
|
||||
hideLoadingIndicator();
|
||||
_container = bindNew<InfiniteScrollingContainer>("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<Database::TrackArtistLinkType> 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<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
|
||||
Artists::addSome()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
for (const Artist::pointer& artist : getArtists(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
|
||||
|
||||
{
|
||||
_container->addWidget(ArtistListHelpers::createEntry(artist));
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto artists {_artistCollector.get(Range {static_cast<std::size_t>(_container->getCount()), _batchSize}, moreResults)};
|
||||
for (const auto& artist : artists)
|
||||
_container->add(ArtistListHelpers::createEntry(artist));
|
||||
}
|
||||
|
||||
if (moreResults)
|
||||
displayLoadingIndicator();
|
||||
else
|
||||
hideLoadingIndicator();
|
||||
_container->setHasMore(moreResults);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -19,69 +19,39 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#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<Database::TrackArtistLinkType> 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<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> getRandomArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||
|
||||
static constexpr Mode defaultMode {Mode::Random};
|
||||
static constexpr std::size_t batchSize {30};
|
||||
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 {};
|
||||
};
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -88,14 +88,14 @@ Explore::Explore(Filters* filters)
|
||||
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
|
||||
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));
|
||||
|
||||
auto artist = std::make_unique<Artist>(_filters);
|
||||
artist->artistsAction.connect(this, &Explore::handleArtistsAction);
|
||||
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);
|
||||
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<Tracks>(_filters);
|
||||
auto tracks = std::make_unique<Tracks>(*_filters);
|
||||
tracks->tracksAction.connect(this, &Explore::handleTracksAction);
|
||||
contentsStack->addWidget(std::move(tracks));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,23 +19,12 @@
|
||||
|
||||
#include "ReleasesView.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#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<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)};
|
||||
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<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
@@ -94,153 +83,55 @@ _filters {filters}
|
||||
popup->popup(moreBtn);
|
||||
});
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("releases");
|
||||
hideLoadingIndicator();
|
||||
_container = bindNew<InfiniteScrollingContainer>("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<Wt::WTemplate>("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<std::size_t>(_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<std::size_t>(_container->getCount()), _batchSize}, moreResults)};
|
||||
for (const auto& release : releases)
|
||||
_container->add(ReleaseListHelpers::createEntry(release));
|
||||
}
|
||||
|
||||
if (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;
|
||||
_container->setHasMore(moreResults);
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Releases::getAllReleases()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
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;
|
||||
return _releaseCollector.getAll();
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
@@ -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<Database::IdType> getAllReleases();
|
||||
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
Starred,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
All
|
||||
};
|
||||
|
||||
void refreshView();
|
||||
void refreshView(Mode mode);
|
||||
void displayLoadingIndicator();
|
||||
void hideLoadingIndicator();
|
||||
|
||||
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();
|
||||
|
||||
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<Mode, std::optional<std::size_t>> 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<Database::IdType> _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
|
||||
|
||||
|
||||
@@ -19,14 +19,19 @@
|
||||
|
||||
#include "SearchView.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
|
||||
#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<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([=]
|
||||
{
|
||||
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
|
||||
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<Wt::WContainerWidget>("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<Wt::WContainerWidget>("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<Wt::WContainerWidget>("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
|
||||
|
||||
|
||||
@@ -22,34 +22,75 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#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<std::string_view> _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<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 addSomeReleases();
|
||||
void addSomeArtists();
|
||||
void addSomeTracks();
|
||||
|
||||
Filters* _filters {};
|
||||
Wt::WMenu* _menu {};
|
||||
ReleaseCollector _releaseCollector;
|
||||
ArtistCollector _artistCollector;
|
||||
TrackCollector _trackCollector;
|
||||
|
||||
std::vector<InfiniteScrollingContainer*> _results;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,57 +19,46 @@
|
||||
|
||||
#include "TracksView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#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<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)};
|
||||
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<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
@@ -97,155 +86,58 @@ _filters {filters}
|
||||
popup->popup(moreBtn);
|
||||
});
|
||||
|
||||
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks");
|
||||
hideLoadingIndicator();
|
||||
_container = bindNew<InfiniteScrollingContainer>("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<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
|
||||
Tracks::addSome()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
bool moreResults {};
|
||||
|
||||
bool moreResults;
|
||||
for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
|
||||
{
|
||||
_tracksContainer->addWidget(TrackListHelpers::createEntry(track, tracksAction));
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto tracks {_trackCollector.get(Range {static_cast<std::size_t>(_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<Database::IdType>
|
||||
Tracks::getAllTracks()
|
||||
{
|
||||
return _trackCollector.getAll();
|
||||
}
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
@@ -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<Database::IdType> 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<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();
|
||||
|
||||
static constexpr Mode defaultMode {Mode::Random};
|
||||
static constexpr std::size_t batchSize {20};
|
||||
static inline std::unordered_map<Mode, std::optional<std::size_t>> 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<Database::IdType> _randomTracks;
|
||||
Wt::WContainerWidget* _tracksContainer {};
|
||||
Wt::WTemplate* _loadingIndicator {};
|
||||
};
|
||||
InfiniteScrollingContainer* _container {};
|
||||
TrackCollector _trackCollector;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
Reference in New Issue
Block a user