UI: Added star/unstar for tracks/albums/artists. ref #74

This commit is contained in:
emeric
2020-09-05 11:05:57 +02:00
parent 5dad7f9211
commit 26d12c39e4
25 changed files with 358 additions and 63 deletions
+3
View File
@@ -128,8 +128,11 @@
<message id="Lms.Explore.recently-added">Recently added</message> <message id="Lms.Explore.recently-added">Recently added</message>
<message id="Lms.Explore.recently-played">Recently played</message> <message id="Lms.Explore.recently-played">Recently played</message>
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.star">Star</message>
<message id="Lms.Explore.starred">Starred</message>
<message id="Lms.Explore.tracks">Tracks</message> <message id="Lms.Explore.tracks">Tracks</message>
<message id="Lms.Explore.type">Type</message> <message id="Lms.Explore.type">Type</message>
<message id="Lms.Explore.unstar">Unstar</message>
<message id="Lms.Explore.value">Value</message> <message id="Lms.Explore.value">Value</message>
<message id="Lms.Explore.various-artists">Various artists</message> <message id="Lms.Explore.various-artists">Various artists</message>
+3
View File
@@ -128,8 +128,11 @@
<message id="Lms.Explore.recently-added">Ajoutés récemment</message> <message id="Lms.Explore.recently-added">Ajoutés récemment</message>
<message id="Lms.Explore.recently-played">Joués récemment</message> <message id="Lms.Explore.recently-played">Joués récemment</message>
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.star">Ajouter aux favoris</message>
<message id="Lms.Explore.starred">Favoris</message>
<message id="Lms.Explore.tracks">Pistes</message> <message id="Lms.Explore.tracks">Pistes</message>
<message id="Lms.Explore.type">Type</message> <message id="Lms.Explore.type">Type</message>
<message id="Lms.Explore.unstar">Retirer des favoris</message>
<message id="Lms.Explore.value">Valeur</message> <message id="Lms.Explore.value">Valeur</message>
<message id="Lms.Explore.various-artists">Artistes divers</message> <message id="Lms.Explore.various-artists">Artistes divers</message>
+57 -1
View File
@@ -303,7 +303,11 @@ Artist::getByFilter(Session& session,
} }
std::vector<Artist::pointer> std::vector<Artist::pointer>
Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) Artist::getLastWritten(Session& session,
std::optional<Wt::WDateTime> after,
const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType,
std::optional<Range> range, bool& moreResults)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -330,6 +334,58 @@ Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, con
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
} }
std::vector<Artist::pointer>
Artist::getStarred(Session& session,
User::pointer user,
const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType,
SortMethod sortMethod,
std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
{
std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN user_artist_starred uas ON uas.artist_id = a.id"
" INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)";
query.bind(user.id());
query.where(oss.str());
}
switch (sortMethod)
{
case Artist::SortMethod::None:
break;
case Artist::SortMethod::ByName:
query.orderBy("name COLLATE NOCASE");
break;
case Artist::SortMethod::BySortName:
query.orderBy("sort_name COLLATE NOCASE");
break;
}
Wt::Dbo::collection<Artist::pointer> collection = query
.groupBy("a.id")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
auto res {std::vector<pointer>(collection.begin(), collection.end())};
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Wt::Dbo::ptr<Release>> std::vector<Wt::Dbo::ptr<Release>>
Artist::getReleases(const std::set<IdType>& clusterIds) const Artist::getReleases(const std::set<IdType>& clusterIds) const
{ {
+40
View File
@@ -245,6 +245,46 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
} }
std::vector<Release::pointer>
Release::getStarred(Session& session,
User::pointer user,
const std::set<IdType>& clusterIds,
std::optional<Range> range,
bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
{
std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN user_release_starred urs ON urs.release_id = r.id"
" INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)";
query.bind(user.id());
query.where(oss.str());
}
Wt::Dbo::collection<Release::pointer> collection = query
.groupBy("r.id")
.orderBy("r.name COLLATE NOCASE")
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1);
auto res {std::vector<pointer>(collection.begin(), collection.end())};
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer> std::vector<Release::pointer>
Release::getByClusters(Session& session, const std::set<IdType>& clusters) Release::getByClusters(Session& session, const std::set<IdType>& clusters)
{ {
+35
View File
@@ -265,6 +265,41 @@ Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
return std::vector<IdType>(res.begin(), res.end()); return std::vector<IdType>(res.begin(), res.end());
} }
std::vector<Track::pointer>
Track::getStarred(Session& session,
Wt::Dbo::ptr<User> user,
const std::set<IdType>& clusterIds,
std::optional<Range> range, bool& moreResults)
{
session.checkSharedLocked();
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
{
std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN user_track_starred uts ON uts.track_id = t.id"
" INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)";
query.bind(user.id());
query.where(oss.str());
}
Wt::Dbo::collection<Track::pointer> collection = query
.offset(range ? static_cast<int>(range->offset) : -1)
.limit(range ? static_cast<int>(range->limit) + 1: -1);
auto res {std::vector<pointer>(collection.begin(), collection.end())};
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Cluster::pointer> std::vector<Cluster::pointer>
Track::getClusters() const Track::getClusters() const
{ {
-23
View File
@@ -172,12 +172,6 @@ User::hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const
return _starredArtists.count(artist) != 0; return _starredArtists.count(artist) != 0;
} }
std::vector<Wt::Dbo::ptr<Artist>>
User::getStarredArtists() const
{
return std::vector<Wt::Dbo::ptr<Artist>>(_starredArtists.begin(), _starredArtists.end());
}
void void
User::starRelease(Wt::Dbo::ptr<Release> release) User::starRelease(Wt::Dbo::ptr<Release> release)
{ {
@@ -198,17 +192,6 @@ User::hasStarredRelease(Wt::Dbo::ptr<Release> release) const
return _starredReleases.count(release) != 0; return _starredReleases.count(release) != 0;
} }
std::vector<Wt::Dbo::ptr<Release>>
User::getStarredReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
{
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> res = _starredReleases.find()
.offset(offset ? static_cast<int>(*offset) : -1)
.limit(limit ? static_cast<int>(*limit) : -1);
return std::vector<Wt::Dbo::ptr<Release>>(res.begin(), res.end());
}
void void
User::starTrack(Wt::Dbo::ptr<Track> track) User::starTrack(Wt::Dbo::ptr<Track> track)
{ {
@@ -229,12 +212,6 @@ User::hasStarredTrack(Wt::Dbo::ptr<Track> track) const
return _starredTracks.count(track) != 0; return _starredTracks.count(track) != 0;
} }
std::vector<Wt::Dbo::ptr<Track>>
User::getStarredTracks() const
{
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
}
} // namespace Database } // namespace Database
@@ -86,6 +86,12 @@ class Artist : public Wt::Dbo::Dbo<Artist>
std::optional<Range>, std::optional<Range>,
bool& moreResults); bool& moreResults);
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {}); static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session,
Wt::Dbo::ptr<User> user,
const std::set<IdType>& clusters,
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod,
std::optional<Range>, bool& moreResults);
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
@@ -59,6 +59,7 @@ class Release : public Wt::Dbo::Dbo<Release>
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {}); static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults); static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}); static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
static std::vector<pointer> getStarred(Session& session, Wt::Dbo::ptr<User> user, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters); static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
static std::vector<pointer> getByFilter(Session& session, static std::vector<pointer> getByFilter(Session& session,
@@ -81,6 +81,10 @@ class Track : public Wt::Dbo::Dbo<Track>
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session); static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {}); static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {}); static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session,
Wt::Dbo::ptr<User> user,
const std::set<IdType>& clusters,
std::optional<Range> range, bool& hasMore);
// Create utility // Create utility
static pointer create(Session& session, const std::filesystem::path& p); static pointer create(Session& session, const std::filesystem::path& p);
@@ -197,18 +197,15 @@ class User : public Wt::Dbo::Dbo<User>
void starArtist(Wt::Dbo::ptr<Artist> artist); void starArtist(Wt::Dbo::ptr<Artist> artist);
void unstarArtist(Wt::Dbo::ptr<Artist> artist); void unstarArtist(Wt::Dbo::ptr<Artist> artist);
bool hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const; bool hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const;
std::vector<Wt::Dbo::ptr<Artist>> getStarredArtists() const;
void starRelease(Wt::Dbo::ptr<Release> release); void starRelease(Wt::Dbo::ptr<Release> release);
void unstarRelease(Wt::Dbo::ptr<Release> release); void unstarRelease(Wt::Dbo::ptr<Release> release);
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const; bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
std::vector<Wt::Dbo::ptr<Release>> getStarredReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
// Stars // Stars
void starTrack(Wt::Dbo::ptr<Track> track); void starTrack(Wt::Dbo::ptr<Track> track);
void unstarTrack(Wt::Dbo::ptr<Track> track); void unstarTrack(Wt::Dbo::ptr<Track> track);
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const; bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
+8 -4
View File
@@ -743,7 +743,8 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
} }
else if (type == "starred") else if (type == "starred")
{ {
releases = user->getStarredReleases(offset, size); bool moreResults {};
releases = Release::getStarred(context.dbSession, user, {}, Range {offset, size}, moreResults);
} }
else if (type == "byGenre") else if (type == "byGenre")
{ {
@@ -1172,19 +1173,22 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
{ {
auto artists {user->getStarredArtists()}; bool moreResults {};
const auto artists {Artist::getStarred(context.dbSession, user, {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, moreResults)};
for (const Artist::pointer& artist : artists) for (const Artist::pointer& artist : artists)
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3)); starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
} }
{ {
auto releases {user->getStarredReleases()}; bool moreResults {};
const auto releases {Release::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)};
for (const Release::pointer& release : releases) for (const Release::pointer& release : releases)
starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
} }
{ {
auto tracks {user->getStarredTracks()}; bool moreResults {};
const auto tracks {Track::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)};
for (const Track::pointer& track : tracks) for (const Track::pointer& track : tracks)
starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
} }
+1
View File
@@ -28,6 +28,7 @@ add_executable(lms
ui/explore/ReleaseView.cpp ui/explore/ReleaseView.cpp
ui/explore/SearchView.cpp ui/explore/SearchView.cpp
ui/explore/TrackListHelpers.cpp ui/explore/TrackListHelpers.cpp
ui/explore/TrackPopup.cpp
ui/explore/TracksView.cpp ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodeResource.cpp ui/resource/AudioTranscodeResource.cpp
+23
View File
@@ -27,6 +27,7 @@
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/ScanSettings.hpp" #include "database/ScanSettings.hpp"
#include "database/User.hpp"
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
@@ -131,6 +132,28 @@ Artist::refreshView()
{ {
artistsAction.emit(PlayQueueAction::PlayLast, {*artistId}); artistsAction.emit(PlayQueueAction::PlayLast, {*artistId});
}); });
bool isStarred {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)})
isStarred = LmsApp->getUser()->hasStarredArtist(artist);
}
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
->triggered().connect(this, [=]
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)};
if (!artist)
return;
if (isStarred)
LmsApp->getUser().modify()->unstarArtist(artist);
else
LmsApp->getUser().modify()->starArtist(artist);
});
popup->addItem(Wt::WString::tr("Lms.Explore.download")) popup->addItem(Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)}); ->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
+10
View File
@@ -59,6 +59,7 @@ Artists::Artists(Filters* filters)
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
@@ -167,6 +168,15 @@ Artists::getArtists(std::optional<Range> range, bool& moreResults)
artists = getRandomArtists(range, moreResults); artists = getRandomArtists(range, moreResults);
break; break;
case Mode::Starred:
artists = Artist::getStarred(LmsApp->getDbSession(),
LmsApp->getUser(),
_filters->getClusterIds(),
linkType,
Artist::SortMethod::BySortName,
range, moreResults);
break;
case Mode::RecentlyPlayed: case Mode::RecentlyPlayed:
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession()) artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
->getArtistsReverse(_filters->getClusterIds(), ->getArtistsReverse(_filters->getClusterIds(),
+1
View File
@@ -46,6 +46,7 @@ class Artists : public Wt::WTemplate
enum class Mode enum class Mode
{ {
Random, Random,
Starred,
RecentlyPlayed, RecentlyPlayed,
RecentlyAdded, RecentlyAdded,
MostPlayed, MostPlayed,
+25
View File
@@ -19,6 +19,8 @@
#include "ReleasePopup.hpp" #include "ReleasePopup.hpp"
#include "database/Release.hpp"
#include "database/User.hpp"
#include "resource/DownloadResource.hpp" #include "resource/DownloadResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -42,6 +44,29 @@ namespace UserInterface
{ {
releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); releasesAction.emit(PlayQueueAction::PlayLast, {releaseId});
}); });
bool isStarred {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)})
isStarred = LmsApp->getUser()->hasStarredRelease(release);
}
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
->triggered().connect(&target, [=]
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
if (!release)
return;
if (isStarred)
LmsApp->getUser().modify()->unstarRelease(release);
else
LmsApp->getUser().modify()->starRelease(release);
});
popup->addItem(Wt::WString::tr("Lms.Explore.download")) popup->addItem(Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(releaseId)}); ->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(releaseId)});
+2 -11
View File
@@ -40,6 +40,7 @@
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "ReleaseListHelpers.hpp" #include "ReleaseListHelpers.hpp"
#include "ReleasePopup.hpp" #include "ReleasePopup.hpp"
#include "TrackPopup.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -241,17 +242,7 @@ Release::refreshView()
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)}; Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
moreBtn->clicked().connect([=]() moreBtn->clicked().connect([=]()
{ {
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; displayTrackPopupMenu(*moreBtn, trackId, tracksAction);
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(moreBtn, [=]
{
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
});
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
popup->popup(moreBtn);
}); });
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
+5
View File
@@ -61,6 +61,7 @@ _filters {filters}
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
@@ -198,6 +199,10 @@ Releases::getReleases(std::optional<Range> range, bool& moreResults)
releases = getRandomReleases(range, moreResults); releases = getRandomReleases(range, moreResults);
break; break;
case Mode::Starred:
releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
break;
case Mode::RecentlyPlayed: case Mode::RecentlyPlayed:
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getReleasesReverse(_filters->getClusterIds(), range, moreResults); releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getReleasesReverse(_filters->getClusterIds(), range, moreResults);
break; break;
+1
View File
@@ -48,6 +48,7 @@ class Releases : public Wt::WTemplate
enum class Mode enum class Mode
{ {
Random, Random,
Starred,
RecentlyPlayed, RecentlyPlayed,
RecentlyAdded, RecentlyAdded,
MostPlayed, MostPlayed,
+3 -12
View File
@@ -31,6 +31,7 @@
#include "resource/ImageResource.hpp" #include "resource/ImageResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "TrackPopup.hpp"
#include "TrackStringUtils.hpp" #include "TrackStringUtils.hpp"
using namespace Database; using namespace Database;
@@ -94,20 +95,10 @@ namespace UserInterface::TrackListHelpers
tracksAction.emit(PlayQueueAction::Play, {trackId}); tracksAction.emit(PlayQueueAction::Play, {trackId});
}); });
Wt::WText* moreBtn = entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
moreBtn->clicked().connect([=, &tracksAction] moreBtn->clicked().connect([=, &tracksAction]
{ {
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; displayTrackPopupMenu(*moreBtn, trackId, tracksAction);
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(moreBtn, [=, &tracksAction]
{
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
});
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
popup->popup(moreBtn);
}); });
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId) LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2020 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 "TrackPopup.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "resource/DownloadResource.hpp"
#include "LmsApplication.hpp"
namespace UserInterface
{
void
displayTrackPopupMenu(Wt::WInteractWidget& target,
Database::IdType trackId,
PlayQueueActionSignal& tracksAction)
{
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
->triggered().connect(&target, [=, &tracksAction]
{
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
});
bool isStarred {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)})
isStarred = LmsApp->getUser()->hasStarredTrack(track);
}
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
->triggered().connect(&target, [=]
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
if (!track)
return;
if (isStarred)
LmsApp->getUser().modify()->unstarTrack(track);
else
LmsApp->getUser().modify()->starTrack(track);
});
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
popup->popup(&target);
}
} // namespace UserInterface
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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/WWidget.h>
#include <Wt/WSignal.h>
#include "database/Types.hpp"
#include "PlayQueueAction.hpp"
namespace UserInterface
{
void displayTrackPopupMenu(Wt::WInteractWidget& target,
Database::IdType releaseId,
PlayQueueActionSignal& releasesAction);
} // namespace UserInterface
+5
View File
@@ -63,6 +63,7 @@ _filters {filters}
}; };
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random); addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed); addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded); addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
@@ -185,6 +186,10 @@ Tracks::getTracks(std::optional<Range> range, bool& moreResults)
tracks = getRandomTracks(range, moreResults); tracks = getRandomTracks(range, moreResults);
break; break;
case Mode::Starred:
tracks = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
break;
case Mode::RecentlyPlayed: case Mode::RecentlyPlayed:
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTracksReverse(_filters->getClusterIds(), range, moreResults); tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTracksReverse(_filters->getClusterIds(), range, moreResults);
break; break;
+1
View File
@@ -47,6 +47,7 @@ class Tracks : public Wt::WTemplate
enum class Mode enum class Mode
{ {
Random, Random,
Starred,
RecentlyPlayed, RecentlyPlayed,
RecentlyAdded, RecentlyAdded,
MostPlayed, MostPlayed,
+19 -9
View File
@@ -1248,22 +1248,26 @@ void
testSingleStarredArtist(Session& session) testSingleStarredArtist(Session& session)
{ {
ScopedArtist artist {session, "MyArtist"}; ScopedArtist artist {session, "MyArtist"};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"}; ScopedUser user {session, "MyUser"};
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)};
user.get().modify()->starArtist(artist.get()); user.get().modify()->starArtist(artist.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto artists {user->getStarredArtists()}; CHECK(user->hasStarredArtist(artist.get()));
bool hasMore {};
auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)};
CHECK(artists.size() == 1); CHECK(artists.size() == 1);
CHECK(artists.front().id() == artist.getId()); CHECK(artists.front().id() == artist.getId());
CHECK(hasMore == false);
CHECK(user->hasStarredArtist(artist.get()));
} }
} }
@@ -1272,22 +1276,26 @@ void
testSingleStarredRelease(Session& session) testSingleStarredRelease(Session& session)
{ {
ScopedRelease release {session, "MyRelease"}; ScopedRelease release {session, "MyRelease"};
ScopedTrack track {session, "MyTrack"};
ScopedUser user {session, "MyUser"}; ScopedUser user {session, "MyUser"};
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
track.get().modify()->setRelease(release.get());
user.get().modify()->starRelease(release.get()); user.get().modify()->starRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto releases {user->getStarredReleases()}; CHECK(user->hasStarredRelease(release.get()));
bool hasMore {};
auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
CHECK(releases.size() == 1); CHECK(releases.size() == 1);
CHECK(releases.front().id() == release.getId()); CHECK(releases.front().id() == release.getId());
CHECK(hasMore == false);
CHECK(user->hasStarredRelease(release.get()));
} }
} }
@@ -1307,11 +1315,13 @@ testSingleStarredTrack(Session& session)
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
auto tracks {user->getStarredTracks()}; CHECK(user->hasStarredTrack(track.get()));
bool hasMore {};
auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
CHECK(tracks.size() == 1); CHECK(tracks.size() == 1);
CHECK(tracks.front().id() == track.getId()); CHECK(tracks.front().id() == track.getId());
CHECK(hasMore == false);
CHECK(user->hasStarredTrack(track.get()));
} }
} }