Refactored the search view. fixes #146
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 "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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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 "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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 "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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user