Added basic UI to display shared playlists, ref #391
This commit is contained in:
@@ -269,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Delete playlist?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Delete playlist?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">My Playlists</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Shared playlists</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||||
|
|||||||
@@ -269,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Supprimer la liste de lecture ?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Supprimer la liste de lecture ?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Mes listes</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Listes partagées</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||||
|
|||||||
@@ -269,6 +269,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Eliminare la playlist?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Eliminare la playlist?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Le mie liste</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Liste condivise</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
|
||||||
|
|||||||
@@ -295,6 +295,8 @@
|
|||||||
|
|
||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Usunąć listę odtwarzania?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Usunąć listę odtwarzania?</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-owned">Moje listy</message>
|
||||||
|
<message id="Lms.Explore.TrackLists.type-shared">Udostępnione listy</message>
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">Szukaj...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Szukaj...</message>
|
||||||
|
|||||||
@@ -270,6 +270,8 @@
|
|||||||
<!--Explore:TrackLists-->
|
<!--Explore:TrackLists-->
|
||||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message>
|
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--Explore:Search-->
|
<!--Explore:Search-->
|
||||||
<message id="Lms.Explore.Search.search-placeholder">搜索...</message>
|
<message id="Lms.Explore.Search.search-placeholder">搜索...</message>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
<li>${play-shuffled class="dropdown-item"}</li>
|
<li>${play-shuffled class="dropdown-item"}</li>
|
||||||
<li>${play-last class="dropdown-item"}</li>
|
<li>${play-last class="dropdown-item"}</li>
|
||||||
<li>${download class="dropdown-item"}</li>
|
<li>${download class="dropdown-item"}</li>
|
||||||
|
${<if-has-delete>}
|
||||||
<li>${delete class="dropdown-item"}</li>
|
<li>${delete class="dropdown-item"}</li>
|
||||||
|
${</if-has-delete>}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-1
@@ -3,12 +3,28 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
|
||||||
<message id="Lms.Explore.TrackLists.template">
|
<message id="Lms.Explore.TrackLists.template">
|
||||||
<div class="d-flex flex-row-reverse">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
${tracklist-type class="me-1"}
|
||||||
|
<div class="d-flex">
|
||||||
|
${search class="form-control form-control-sm me-1" type="search"}
|
||||||
${sort-mode}
|
${sort-mode}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
${tracklists}
|
${tracklists}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.TrackLists.template.type-selector">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
${selected-item}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>${owned class="dropdown-item"}</li>
|
||||||
|
<li>${shared class="dropdown-item"}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.TrackLists.template.sort-mode">
|
<message id="Lms.Explore.TrackLists.template.sort-mode">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ namespace lms::db
|
|||||||
template<typename ResultType>
|
template<typename ResultType>
|
||||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params)
|
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params)
|
||||||
{
|
{
|
||||||
|
assert(!params.user.isValid() || !params.excludedUser.isValid());
|
||||||
|
|
||||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM tracklist t_l") };
|
||||||
|
|
||||||
if (!params.clusters.empty() || params.mediaLibrary.isValid())
|
if (!params.clusters.empty() || params.mediaLibrary.isValid())
|
||||||
@@ -46,6 +48,9 @@ namespace lms::db
|
|||||||
query.groupBy("t_l.id");
|
query.groupBy("t_l.id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (std::string_view keyword : params.keywords)
|
||||||
|
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||||
|
|
||||||
if (params.mediaLibrary.isValid())
|
if (params.mediaLibrary.isValid())
|
||||||
query.join("track t ON t.id = t_l_e.track_id");
|
query.join("track t ON t.id = t_l_e.track_id");
|
||||||
|
|
||||||
@@ -54,6 +59,8 @@ namespace lms::db
|
|||||||
|
|
||||||
if (params.user.isValid())
|
if (params.user.isValid())
|
||||||
query.where("t_l.user_id = ?").bind(params.user);
|
query.where("t_l.user_id = ?").bind(params.user);
|
||||||
|
else if (params.excludedUser.isValid())
|
||||||
|
query.where("t_l.user_id <> ? OR t_l.user_id IS NULL").bind(params.excludedUser);
|
||||||
|
|
||||||
if (params.type)
|
if (params.type)
|
||||||
query.where("t_l.type = ?").bind(*params.type);
|
query.where("t_l.type = ?").bind(*params.type);
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ namespace lms::db
|
|||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
||||||
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
std::optional<TrackListType> type;
|
std::optional<TrackListType> type;
|
||||||
UserId user; // only tracklists owned by this user
|
UserId user; // only tracklists owned by this user
|
||||||
|
UserId excludedUser; // only tracklists *not* owned by this user
|
||||||
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
|
MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library
|
||||||
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
||||||
std::optional<Visibility> visibility;
|
std::optional<Visibility> visibility;
|
||||||
@@ -75,6 +77,11 @@ namespace lms::db
|
|||||||
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
|
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
|
||||||
|
{
|
||||||
|
keywords = _keywords;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
FindParameters& setRange(std::optional<Range> _range)
|
FindParameters& setRange(std::optional<Range> _range)
|
||||||
{
|
{
|
||||||
range = _range;
|
range = _range;
|
||||||
@@ -90,6 +97,12 @@ namespace lms::db
|
|||||||
user = _user;
|
user = _user;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setExcludedUser(UserId _user)
|
||||||
|
{
|
||||||
|
excludedUser = _user;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
||||||
{
|
{
|
||||||
mediaLibrary = _mediaLibrary;
|
mediaLibrary = _mediaLibrary;
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ namespace lms::ui
|
|||||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
Wt::WLineEdit* searEdit{ bindNew<Wt::WLineEdit>("search") };
|
Wt::WLineEdit* searchEdit{ bindNew<Wt::WLineEdit>("search") };
|
||||||
searEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||||
searEdit->textInput().connect([this, searEdit] {
|
searchEdit->textInput().connect([this, searchEdit] {
|
||||||
refreshView(searEdit->text());
|
refreshView(searchEdit->text());
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ namespace lms::ui
|
|||||||
if (!trackList)
|
if (!trackList)
|
||||||
throw TrackListNotFoundException{};
|
throw TrackListNotFoundException{};
|
||||||
|
|
||||||
|
if (trackList->getUserId().isValid()
|
||||||
|
&& LmsApp->getUserId() != trackList->getUserId()
|
||||||
|
&& trackList->getVisibility() != db::TrackList::Visibility::Public)
|
||||||
|
{
|
||||||
|
throw TrackListNotFoundException{};
|
||||||
|
}
|
||||||
|
|
||||||
LmsApp->setTitle(std::string{ trackList->getName() });
|
LmsApp->setTitle(std::string{ trackList->getName() });
|
||||||
_trackListId = *trackListId;
|
_trackListId = *trackListId;
|
||||||
|
|
||||||
@@ -133,6 +140,9 @@ namespace lms::ui
|
|||||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||||
->setLink(Wt::WLink{ std::make_unique<DownloadTrackListResource>(*trackListId) });
|
->setLink(Wt::WLink{ std::make_unique<DownloadTrackListResource>(*trackListId) });
|
||||||
|
|
||||||
|
if (trackList->getUserId() == LmsApp->getUserId())
|
||||||
|
{
|
||||||
|
setCondition("if-has-delete", true);
|
||||||
bindNew<Wt::WPushButton>("delete", Wt::WString::tr("Lms.delete"))
|
bindNew<Wt::WPushButton>("delete", Wt::WString::tr("Lms.delete"))
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this, trackListId] {
|
.connect([this, trackListId] {
|
||||||
@@ -164,6 +174,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
LmsApp->getModalManager().show(std::move(modal));
|
LmsApp->getModalManager().show(std::move(modal));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.TrackList.template.entry-container"));
|
_container = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.Explore.TrackList.template.entry-container"));
|
||||||
_container->onRequestElements.connect([this] {
|
_container->onRequestElements.connect([this] {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "TrackListsView.hpp"
|
#include "TrackListsView.hpp"
|
||||||
|
|
||||||
|
#include <Wt/WLineEdit.h>
|
||||||
#include <Wt/WPushButton.h>
|
#include <Wt/WPushButton.h>
|
||||||
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
@@ -44,16 +45,38 @@ namespace lms::ui
|
|||||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
{
|
{
|
||||||
_mode = state::readValue<Mode>("tracklists_sort_mode").value_or(_defaultMode);
|
_type = state::readValue<Type>("tracklists_type").value_or(_defaultType);
|
||||||
|
|
||||||
using SortModeSelector = DropDownMenuSelector<TrackLists::Mode>;
|
using TypeSelector = DropDownMenuSelector<Type>;
|
||||||
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", Wt::WString::tr("Lms.Explore.TrackLists.template.sort-mode"), _mode) };
|
TypeSelector* typeSelector{ bindNew<TypeSelector>("tracklist-type", Wt::WString::tr("Lms.Explore.TrackLists.template.type-selector"), _type) };
|
||||||
sortModeSelector->bindItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), Mode::RecentlyModified);
|
typeSelector->bindItem("owned", Wt::WString::tr("Lms.Explore.TrackLists.type-owned"), Type::Owned);
|
||||||
sortModeSelector->bindItem("all", Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
typeSelector->bindItem("shared", Wt::WString::tr("Lms.Explore.TrackLists.type-shared"), Type::Shared);
|
||||||
|
|
||||||
sortModeSelector->itemSelected.connect(this, [this](TrackLists::Mode mode) {
|
typeSelector->itemSelected.connect(this, [this](Type type) {
|
||||||
state::writeValue<Mode>("tracklists_sort_mode", mode);
|
state::writeValue<Type>("tracklists_type", type);
|
||||||
_mode = mode;
|
_type = type;
|
||||||
|
refreshView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WLineEdit* searchEdit{ bindNew<Wt::WLineEdit>("search") };
|
||||||
|
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||||
|
searchEdit->textInput().connect([this, searchEdit] {
|
||||||
|
_searchText = searchEdit->text().toUTF8();
|
||||||
|
refreshView();
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
_sortMode = state::readValue<SortMode>("tracklists_sort_mode").value_or(_defaultSortMode);
|
||||||
|
|
||||||
|
using SortModeSelector = DropDownMenuSelector<SortMode>;
|
||||||
|
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", Wt::WString::tr("Lms.Explore.TrackLists.template.sort-mode"), _sortMode) };
|
||||||
|
sortModeSelector->bindItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), SortMode::RecentlyModified);
|
||||||
|
sortModeSelector->bindItem("all", Wt::WString::tr("Lms.Explore.all"), SortMode::All);
|
||||||
|
|
||||||
|
sortModeSelector->itemSelected.connect(this, [this](SortMode mode) {
|
||||||
|
state::writeValue<SortMode>("tracklists_sort_mode", mode);
|
||||||
|
_sortMode = mode;
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,17 +117,32 @@ namespace lms::ui
|
|||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
TrackList::FindParameters params;
|
TrackList::FindParameters params;
|
||||||
|
|
||||||
|
if (!_searchText.empty())
|
||||||
|
params.setKeywords(core::stringUtils::splitString(_searchText, ' '));
|
||||||
params.setClusters(_filters.getClusters());
|
params.setClusters(_filters.getClusters());
|
||||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||||
params.setUser(LmsApp->getUserId());
|
|
||||||
params.setType(TrackListType::PlayList);
|
params.setType(TrackListType::PlayList);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
switch (_mode)
|
|
||||||
|
switch (_type)
|
||||||
{
|
{
|
||||||
case Mode::All:
|
case Type::Owned:
|
||||||
|
params.setUser(LmsApp->getUserId());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Type::Shared:
|
||||||
|
params.setExcludedUser(LmsApp->getUserId());
|
||||||
|
params.setVisibility(db::TrackList::Visibility::Public);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_sortMode)
|
||||||
|
{
|
||||||
|
case SortMode::All:
|
||||||
params.setSortMethod(TrackListSortMethod::Name);
|
params.setSortMethod(TrackListSortMethod::Name);
|
||||||
break;
|
break;
|
||||||
case Mode::RecentlyModified:
|
case SortMode::RecentlyModified:
|
||||||
params.setSortMethod(TrackListSortMethod::LastModifiedDesc);
|
params.setSortMethod(TrackListSortMethod::LastModifiedDesc);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,21 +46,30 @@ namespace lms::ui
|
|||||||
void onTrackListDeleted(db::TrackListId trackListId);
|
void onTrackListDeleted(db::TrackListId trackListId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Mode
|
enum class SortMode
|
||||||
{
|
{
|
||||||
RecentlyModified,
|
RecentlyModified,
|
||||||
All,
|
All,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
Owned,
|
||||||
|
Shared,
|
||||||
|
};
|
||||||
|
|
||||||
void refreshView();
|
void refreshView();
|
||||||
void addSome();
|
void addSome();
|
||||||
void addTracklist(const db::ObjectPtr<db::TrackList>& trackList);
|
void addTracklist(const db::ObjectPtr<db::TrackList>& trackList);
|
||||||
|
|
||||||
static constexpr Mode _defaultMode{ Mode::RecentlyModified };
|
static constexpr SortMode _defaultSortMode{ SortMode::RecentlyModified };
|
||||||
|
static constexpr Type _defaultType{ Type::Owned };
|
||||||
static constexpr std::size_t _batchSize{ 30 };
|
static constexpr std::size_t _batchSize{ 30 };
|
||||||
static constexpr std::size_t _maxCount{ 500 };
|
static constexpr std::size_t _maxCount{ 500 };
|
||||||
|
|
||||||
Mode _mode;
|
SortMode _sortMode{ _defaultSortMode };
|
||||||
|
Type _type{ _defaultType };
|
||||||
|
std::string _searchText;
|
||||||
Filters& _filters;
|
Filters& _filters;
|
||||||
Wt::WWidget* _currentActiveItem{};
|
Wt::WWidget* _currentActiveItem{};
|
||||||
InfiniteScrollingContainer* _container{};
|
InfiniteScrollingContainer* _container{};
|
||||||
|
|||||||
Reference in New Issue
Block a user