Added a message box to display some track information. closes #276

This commit is contained in:
emeric
2022-11-21 21:04:23 +01:00
parent a2b1199e87
commit 2b7d3bfaf4
35 changed files with 573 additions and 180 deletions
+52 -23
View File
@@ -23,6 +23,7 @@ extern "C"
{
#define __STDC_CONSTANT_MACROS
#include <libavcodec/avcodec.h>
#include <libavcodec/codec_par.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
}
@@ -38,7 +39,7 @@ static std::string averror_to_string(int error)
{
std::array<char, 128> buf = {0};
if (av_strerror(error, buf.data(), buf.size()) == 0)
if (::av_strerror(error, buf.data(), buf.size()) == 0)
return &buf[0];
else
return "Unknown error";
@@ -104,7 +105,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& re
return;
AVDictionaryEntry *tag = NULL;
while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
{
res[StringUtils::stringToUpper(tag->key)] = tag->value;
}
@@ -140,31 +141,18 @@ AudioFile::getStreamInfo() const
for (std::size_t i {}; i < _context->nb_streams; ++i)
{
AVStream* avstream { _context->streams[i]};
// Skip attached pics
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
continue;
if (!avstream->codecpar)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
continue;
}
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
continue;
res.push_back( {i, static_cast<std::size_t>(avstream->codecpar->bit_rate)} );
std::optional<StreamInfo> streamInfo {getStreamInfo(i)};
if (streamInfo)
res.emplace_back(std::move(*streamInfo));
}
return res;
}
std::optional<std::size_t>
AudioFile::getBestStream() const
AudioFile::getBestStreamIndex() const
{
int res = av_find_best_stream(_context,
int res = ::av_find_best_stream(_context,
AVMEDIA_TYPE_AUDIO,
-1, // Auto
-1, // Auto
@@ -177,6 +165,18 @@ AudioFile::getBestStream() const
return res;
}
std::optional<StreamInfo>
AudioFile::getBestStreamInfo() const
{
std::optional<StreamInfo> res;
std::optional<std::size_t> bestStreamIndex {getBestStreamIndex()};
if (bestStreamIndex)
res = getStreamInfo(*bestStreamIndex);
return res;
}
bool
AudioFile::hasAttachedPictures() const
{
@@ -238,10 +238,39 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
}
}
std::optional<AudioFileFormat>
guessMediaFileFormat(const std::filesystem::path& file)
std::optional<StreamInfo>
AudioFile::getStreamInfo(std::size_t streamIndex) const
{
const AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)};
std::optional<StreamInfo> res;
AVStream* avstream { _context->streams[streamIndex]};
assert(avstream);
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
return res;
if (!avstream->codecpar)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
return res;
}
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
return res;
res.emplace();
res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codec.empty());
return res;
}
std::optional<AudioFileFormat>
guessAudioFileFormat(const std::filesystem::path& file)
{
const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)};
if (!format || !format->name)
return {};
+4 -1
View File
@@ -43,11 +43,14 @@ namespace Av
std::chrono::milliseconds getDuration() const override;
MetadataMap getMetaData() const override;
std::vector<StreamInfo> getStreamInfo() const override;
std::optional<std::size_t> getBestStream() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
private:
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p;
AVFormatContext* _context {};
};
+6 -4
View File
@@ -37,13 +37,14 @@ namespace Av
{
std::string mimeType;
const std::byte* data {};
std::size_t dataSize;
std::size_t dataSize {};
};
struct StreamInfo
{
size_t id;
std::size_t bitrate;
size_t index {};
std::size_t bitrate {};
std::string codec;
};
class IAudioFile
@@ -57,7 +58,8 @@ namespace Av
virtual std::chrono::milliseconds getDuration() const = 0;
virtual MetadataMap getMetaData() const = 0;
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
virtual std::optional<std::size_t> getBestStream() const = 0; // none if failure/unknown
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
};
+9 -1
View File
@@ -94,7 +94,10 @@ createQuery(Session& session, const Artist::FindParameters& params)
session.checkSharedLocked();
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType)
if (params.sortMethod == ArtistSortMethod::LastWritten
|| params.writtenAfter.isValid()
|| params.linkType
|| params.track.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
@@ -157,6 +160,11 @@ createQuery(Session& session, const Artist::FindParameters& params)
query.where(oss.str());
}
if (params.track.isValid())
{
query.where("t.id = ?").bind(params.track);
}
switch (params.sortMethod)
{
case ArtistSortMethod::None:
@@ -33,6 +33,7 @@
#include "services/database/ReleaseId.hpp"
#include "services/database/Types.hpp"
#include "services/database/UserId.hpp"
#include "services/database/TrackId.hpp"
#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp"
@@ -61,6 +62,7 @@ class Artist : public Object<Artist, ArtistId>
Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler
TrackId track; // artists involved in this track
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
@@ -69,6 +71,7 @@ class Artist : public Object<Artist, ArtistId>
FindParameters& setRange(Range _range) {range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
};
Artist() = default;
+10 -4
View File
@@ -92,21 +92,27 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
}
{
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
}
{
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))};
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))};
EXPECT_EQ(tracks.results.size(), 0);
}
{
auto transaction {session.createSharedTransaction()};
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))};
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
}
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
@@ -198,7 +204,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
}
{
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createSharedTransaction()};
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))};
ASSERT_EQ(tracks.results.size(), 1);
-65
View File
@@ -278,71 +278,6 @@ LmsApplication::finalize()
preQuit().emit();
}
Wt::WLink
LmsApplication::createArtistLink(Database::Artist::pointer artist)
{
if (const auto mbid {artist->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()};
}
std::unique_ptr<Wt::WAnchor>
LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText)
{
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
if (addText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(artist->getName()));
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
}
return res;
}
Wt::WLink
LmsApplication::createReleaseLink(Database::Release::pointer release)
{
if (const auto mbid {release->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()};
}
std::unique_ptr<Wt::WAnchor>
LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool addText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
if (addText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName()));
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
}
return res;
}
std::unique_ptr<Wt::WAnchor>
LmsApplication::createTrackListAnchor(Database::TrackList::pointer trackList, bool addText)
{
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
auto res {std::make_unique<Wt::WAnchor>(link)};
if (addText)
{
const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})};
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(name);
res->setToolTip(name, Wt::TextFormat::Plain);
}
return res;
}
void
LmsApplication::handleException(LmsApplicationException& e)
{
-10
View File
@@ -31,12 +31,8 @@
namespace Database
{
class Artist;
class Cluster;
class Db;
class Release;
class Session;
class TrackList;
class User;
}
@@ -80,12 +76,6 @@ class LmsApplication : public Wt::WApplication
// Used to classify the message sent to the user
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
static Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool addText = true);
static std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::ObjectPtr<Database::TrackList> trackList, bool addText = true);
MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
PlayQueue& getPlayQueue() const { return *_playQueue; }
ModalManager& getModalManager() const { return *_modalManager; }
+4 -3
View File
@@ -42,6 +42,7 @@
#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
#include "Utils.hpp"
namespace UserInterface {
@@ -155,7 +156,7 @@ replayGainPreAmpGainFromString(const std::string& str)
if (!value)
return std::nullopt;
return Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
return ::Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
}
static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings)
@@ -281,7 +282,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
_artist->setLink(Utils::createArtistLink(artists.front()));
}
else
{
@@ -293,7 +294,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
_release->setLink(Utils::createReleaseLink(track->getRelease()));
}
else
{
+2 -2
View File
@@ -500,9 +500,9 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
if (release)
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
entry->bindWidget("release", Utils::createReleaseAnchor(release));
{
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
anchor->setImage(std::move(cover));
+126
View File
@@ -22,10 +22,17 @@
#include <iomanip>
#include <sstream>
#include <Wt/WAnchor.h>
#include <Wt/WText.h>
#include "services/database/Artist.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Release.hpp"
#include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "explore/Filters.hpp"
#include "LmsApplication.hpp"
namespace UserInterface::Utils
@@ -112,4 +119,123 @@ namespace UserInterface::Utils
return res;
}
std::unique_ptr<Wt::WContainerWidget>
createArtistContainer(const std::vector<Database::ArtistId>& artistIds)
{
using namespace Database;
std::unique_ptr<Wt::WContainerWidget> artistContainer {std::make_unique<Wt::WContainerWidget>()};
bool firstArtist {true};
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
for (const ArtistId artistId : artistIds)
{
const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)};
if (!artist)
continue;
if (!firstArtist)
artistContainer->addNew<Wt::WText>(" · ");
auto anchor {createArtistAnchor(artist)};
anchor->addStyleClass("link-success text-decoration-none"); // hack
artistContainer->addWidget(std::move(anchor));
firstArtist = false;
}
return artistContainer;
}
Wt::WLink
createArtistLink(Database::Artist::pointer artist)
{
if (const auto mbid {artist->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()};
}
std::unique_ptr<Wt::WAnchor>
createArtistAnchor(Database::Artist::pointer artist, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(artist->getName()));
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
}
return res;
}
Wt::WLink
createReleaseLink(Database::Release::pointer release)
{
if (const auto mbid {release->getMBID()})
return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}};
else
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()};
}
std::unique_ptr<Wt::WAnchor>
createReleaseAnchor(Database::Release::pointer release, bool setText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName()));
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
}
return res;
}
std::unique_ptr<Wt::WAnchor>
createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
{
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
auto res {std::make_unique<Wt::WAnchor>(link)};
if (setText)
{
const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})};
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(name);
res->setToolTip(name, Wt::TextFormat::Plain);
}
return res;
}
std::unique_ptr<Wt::WContainerWidget>
createClustersForTrack(Database::Track::pointer track, Filters& filters)
{
using namespace Database;
std::unique_ptr<Wt::WContainerWidget> clusterContainer {std::make_unique<Wt::WContainerWidget>()};
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
const auto clusterGroups {track->getClusterGroups(clusterTypes, 3)};
for (const auto& clusters : clusterGroups)
{
for (const Cluster::pointer& cluster : clusters)
{
const ClusterId clusterId {cluster->getId()};
Wt::WInteractWidget* entry {clusterContainer->addWidget(Utils::createCluster(clusterId))};
entry->clicked().connect([&filters, clusterId]
{
filters.add(clusterId);
});
}
}
return clusterContainer;
}
}
+28 -2
View File
@@ -22,22 +22,48 @@
#include <chrono>
#include <string>
#include <Wt/WContainerWidget.h>
#include <Wt/WImage.h>
#include <Wt/WInteractWidget.h>
#include "services/database/ArtistId.hpp"
#include "services/database/ClusterId.hpp"
#include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "resource/CoverResource.hpp"
namespace Database
{
class Artist;
class Cluster;
class Release;
class Track;
class TrackList;
}
namespace UserInterface
{
class Filters;
}
namespace UserInterface::Utils
{
std::string
durationToString(std::chrono::milliseconds msDuration);
std::string durationToString(std::chrono::milliseconds msDuration);
std::unique_ptr<Wt::WImage> createCover(Database::ReleaseId releaseId, CoverResource::Size size);
std::unique_ptr<Wt::WImage> createCover(Database::TrackId trackId, CoverResource::Size size);
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete = false);
std::unique_ptr<Wt::WContainerWidget> createArtistContainer(const std::vector<Database::ArtistId>& artists);
Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool setText = true);
Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool setText = true);
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::ObjectPtr<Database::TrackList> trackList, bool setText = true);
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(Database::ObjectPtr<Database::Track> track, Filters& filters);
}
+6 -6
View File
@@ -53,7 +53,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
static inline constexpr Field UpdatePeriodField {"update-period"};
static inline constexpr Field UpdateStartTimeField {"update-start-time"};
static inline constexpr Field RecommendationEngineTypeField {"recommendation-engine-type"};
static inline constexpr Field TagsField {"tags"};
static inline constexpr Field ClustersField {"clusters"};
using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
@@ -65,7 +65,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
addField(UpdatePeriodField);
addField(UpdateStartTimeField);
addField(RecommendationEngineTypeField);
addField(TagsField);
addField(ClustersField);
auto dirValidator {createDirectoryValidator()};
dirValidator->setMandatory(true);
@@ -114,7 +114,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
{
std::vector<std::string> names;
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); });
setValue(TagsField, StringUtils::joinStrings(names, " "));
setValue(ClustersField, StringUtils::joinStrings(names, " "));
}
}
@@ -138,7 +138,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
if (recommendationEngineTypeRow)
scanSettings.modify()->setRecommendationEngineType(_recommendationEngineTypeModel->getValue(*recommendationEngineTypeRow));
auto clusterTypes {StringUtils::splitStringCopy(valueText(TagsField).toUTF8(), " ")};
auto clusterTypes {StringUtils::splitStringCopy(valueText(ClustersField).toUTF8(), " ")};
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
}
@@ -215,8 +215,8 @@ DatabaseSettingsView::refreshView()
recommendationEngineType->setModel(model->recommendationEngineTypeModel());
t->setFormWidget(DatabaseSettingsModel::RecommendationEngineTypeField, std::move(recommendationEngineType));
// Tags
t->setFormWidget(DatabaseSettingsModel::TagsField, std::make_unique<Wt::WLineEdit>());
// Clusters
t->setFormWidget(DatabaseSettingsModel::ClustersField, std::make_unique<Wt::WLineEdit>());
// Buttons
Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
+2 -2
View File
@@ -21,7 +21,7 @@
#include <Wt/WAnchor.h>
#include "services/database/Artist.hpp"
#include "LmsApplication.hpp"
#include "Utils.hpp"
namespace UserInterface::ArtistListHelpers
{
@@ -29,7 +29,7 @@ namespace UserInterface::ArtistListHelpers
createEntry(const Database::ObjectPtr<Database::Artist>& artist)
{
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
res->bindWidget("name", Utils::createArtistAnchor(artist));
return res;
}
+1 -1
View File
@@ -335,7 +335,7 @@ Artist::addSomeNonReleaseTracks()
}
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController));
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
areTracksAdded = true;
}
+1 -1
View File
@@ -137,7 +137,7 @@ Artists::refreshArtistLinkTypes()
if (!usedLinkTypes.contains(linkType))
return;
linkTypeModel->add(Wt::WString::tr(std::string {stringKey}), linkType);
linkTypeModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
}};
linkTypeModel->clear();
+3 -4
View File
@@ -26,7 +26,6 @@
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "LmsApplication.hpp"
#include "Utils.hpp"
using namespace Database;
@@ -39,11 +38,11 @@ namespace UserInterface::ReleaseListHelpers
{
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey))};
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
entry->bindWidget("release-name", Utils::createReleaseAnchor(release));
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
{
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Large)};
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
anchor->setImage(std::move(cover));
@@ -62,7 +61,7 @@ namespace UserInterface::ReleaseListHelpers
else if (artists.size() == 1 && !isSameArtist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front()));
}
if (showYear)
+6 -2
View File
@@ -37,6 +37,7 @@
#include "explore/Filters.hpp"
#include "explore/PlayQueueController.hpp"
#include "explore/ReleaseListHelpers.hpp"
#include "explore/TrackListHelpers.hpp"
#include "LmsApplication.hpp"
#include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp"
@@ -270,7 +271,7 @@ Release::refreshView()
{
if (!firstArtist)
artistsContainer->addNew<Wt::WText>(" · ");
auto anchor {LmsApplication::createArtistAnchor(artist)};
auto anchor {Utils::createArtistAnchor(artist)};
anchor->addStyleClass("link-success text-decoration-none"); // hack
artistsContainer->addWidget(std::move(anchor));
firstArtist = false;
@@ -319,6 +320,9 @@ Release::refreshView()
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
->clicked().connect([=] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
}
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
@@ -361,7 +365,7 @@ Release::refreshReleaseArtists(const Database::Release::pointer& release)
for (const auto& artist : artists)
{
Wt::WTemplate* artistTemplate {artistsContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-release-artist"))};
artistTemplate->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
artistTemplate->bindWidget("artist", Utils::createArtistAnchor(artist));
}
}
}
+2 -1
View File
@@ -38,6 +38,7 @@ namespace UserInterface
SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
, _playQueueController {playQueueController}
, _filters {filters}
, _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
, _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
, _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
@@ -152,7 +153,7 @@ namespace UserInterface
for (const TrackId trackId : trackIds.results)
{
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController));
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
}
}
+1
View File
@@ -72,6 +72,7 @@ namespace UserInterface
void addSomeTracks();
PlayQueueController& _playQueueController;
Filters& _filters;
ArtistCollector _artistCollector;
ReleaseCollector _releaseCollector;
TrackCollector _trackCollector;
+75 -4
View File
@@ -23,6 +23,7 @@
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
#include "av/IAudioFile.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
@@ -36,29 +37,96 @@
#include "resource/CoverResource.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "ModalManager.hpp"
#include "Utils.hpp"
using namespace Database;
namespace UserInterface::TrackListHelpers
{
void
showTrackInfoModal(Database::TrackId trackId, Filters& filters)
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
if (!track)
return;
auto trackInfo {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-info"))};
Wt::WWidget* trackInfoPtr {trackInfo.get()};
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")};
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{
Artist::FindParameters params;
params.setTrack(trackId);
params.setLinkType(linkType);
const auto artistIds {Artist::find(LmsApp->getDbSession(), params)};
if (artistIds.results.empty())
return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-info.artists"))};
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
};
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
if (const auto audioFile {Av::parseAudioFile(track->getPath())})
{
const std::optional<Av::StreamInfo> audioStream {audioFile->getBestStreamInfo()};
if (audioStream)
{
trackInfo->setCondition("if-has-codec", true);
trackInfo->bindString("codec", audioStream->codec);
if (audioStream->bitrate)
{
trackInfo->setCondition("if-has-bitrate", true);
trackInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
}
}
}
trackInfo->bindString("duration", Utils::durationToString(track->getDuration()));
Wt::WContainerWidget* clusterContainer {trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters))};
if (clusterContainer->count() > 0)
trackInfo->setCondition("if-has-clusters", true);
Wt::WPushButton* okBtn {trackInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok"))};
okBtn->clicked().connect([=]
{
LmsApp->getModalManager().dispose(trackInfoPtr);
});
LmsApp->getModalManager().show(std::move(trackInfo));
}
std::unique_ptr<Wt::WWidget>
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController)
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters)
{
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
auto* entryPtr {entry.get()};
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists {track->getArtists({TrackArtistLinkType::Artist})};
const Release::pointer release {track->getRelease()};
const TrackId trackId {track->getId()};
if (track->getRelease())
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
entry->bindWidget("release", Utils::createReleaseAnchor(track->getRelease()));
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
anchor->setImage(std::move((cover)));
@@ -109,6 +177,9 @@ namespace UserInterface::TrackListHelpers
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
->clicked().connect([trackId, &filters] { showTrackInfoModal(trackId, filters); });
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::TrackId loadedTrackId)
{
entryPtr->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
+4 -1
View File
@@ -23,6 +23,7 @@
#include <Wt/WWidget.h>
#include "services/database/Object.hpp"
#include "services/database/TrackId.hpp"
namespace Database
{
@@ -32,10 +33,12 @@ namespace Database
namespace UserInterface
{
class PlayQueueController;
class Filters;
}
namespace UserInterface::TrackListHelpers
{
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController);
void showTrackInfoModal(Database::TrackId trackId, Filters& filters);
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters);
} // namespace UserInterface
+1 -1
View File
@@ -199,7 +199,7 @@ namespace UserInterface
for (const TrackId trackId : trackIds.results)
{
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
}
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
+1 -1
View File
@@ -136,7 +136,7 @@ namespace UserInterface
const TrackListId trackListId {trackList->getId()};
WTemplate* entry {_container->addNew<Template>(Wt::WString::tr("Lms.Explore.TrackLists.template.entry"))};
entry->bindWidget("name", LmsApplication::createTrackListAnchor(trackList));
entry->bindWidget("name", Utils::createTrackListAnchor(trackList));
assert(_trackListWidgets.find(trackListId) == std::cend(_trackListWidgets));
_trackListWidgets.emplace(trackListId, entry);
+2 -1
View File
@@ -37,6 +37,7 @@ namespace UserInterface {
Tracks::Tracks(Filters& filters, PlayQueueController& playQueueController)
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")}
, _filters {filters}
, _playQueueController {playQueueController}
, _trackCollector {filters, _defaultMode, _maxCount}
{
@@ -124,7 +125,7 @@ Tracks::addSome()
for (const TrackId trackId : trackIds.results)
{
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
}
_container->setHasMore(trackIds.moreResults);
+1
View File
@@ -46,6 +46,7 @@ namespace UserInterface
static constexpr std::size_t _batchSize {6};
static constexpr std::size_t _maxCount {8000};
Filters& _filters;
PlayQueueController& _playQueueController;
Wt::WWidget* _currentActiveItem {};
InfiniteScrollingContainer* _container {};