From 9ec42ade17c386508faa61b12a5af16e97810081 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Dec 2014 01:51:58 +0100 Subject: [PATCH 1/3] [UI] The playqueue now displays track/artist names on two lines --- src/ui/audio/PlayQueue.cpp | 89 +++++++++++++++++++++++++++++--------- src/ui/audio/PlayQueue.hpp | 2 + src/ui/docroot/css/lms.css | 19 ++++++++ 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 3e17d9ef..9c4beb4b 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "resource/CoverResource.hpp" @@ -34,6 +35,7 @@ #include "PlayQueue.hpp" static const int CoverRole = Wt::UserRole + 1; +static const int NameRole = Wt::UserRole + 2; namespace { @@ -47,17 +49,20 @@ namespace { Wt::WModelIndex index1 = model->index(row1, i); Wt::WModelIndex index2 = model->index(row2, i); + // swap data associated with standard roles { auto tmp = model->itemData(index1); // 1 -> tmp model->setItemData(index1, model->itemData(index2)); // 2 -> 1 model->setItemData(index2, tmp); // tmp-> 2 } - // Workaround, do the same thing for the CoverRole + // swap data associated with our custom roles + const std::vector(roles) = {CoverRole, NameRole}; + BOOST_FOREACH(int role, roles) { - auto tmp = model->data(index1, CoverRole); - model->setData(index1, model->data(index2, CoverRole), CoverRole); - model->setData(index2, tmp, CoverRole); + auto tmp = model->data(index1, role); + model->setData(index1, model->data(index2, role), role); + model->setData(index2, tmp, role); } } } @@ -68,10 +73,8 @@ namespace UserInterface { enum ColumnId { COLUMN_ID_TRACK_ID = 0, - COLUMN_ID_POS = 1, - COLUMN_ID_COVER = 2, - COLUMN_ID_NAME = 3, - COLUMN_ID_DURATION = 4 + COLUMN_ID_COVER = 1, + COLUMN_ID_NAME = 2, }; static const int trackPosInvalid = -1; @@ -227,6 +230,12 @@ TrackSelector::setSize(std::size_t size) refreshPositions(); } +struct Name +{ + Wt::WString track; + Wt::WString artist; +}; + class PlayQueueItemDelegate : public Wt::WItemDelegate { public: @@ -249,8 +258,43 @@ class PlayQueueItemDelegate : public Wt::WItemDelegate image->setImageLink(Wt::WLink(resource)); + // Apply style if any + Wt::WString styleClass = Wt::asString(index.data(Wt::StyleClassRole)); + + // Apply selection style if any + if (flags & Wt::RenderSelected) + styleClass += " " + Wt::WApplication::instance()->theme()->activeClass(); + + image->setStyleClass(styleClass); + res = image; res->setObjectName("z"); + + } + else if (!index.data(NameRole).empty()) + { + Name name = boost::any_cast(index.data(NameRole)); + + Wt::WContainerWidget *container = new Wt::WContainerWidget(); + Wt::WText* track = new Wt::WText(name.track, Wt::PlainText, container); + Wt::WText* artist = new Wt::WText(name.artist, Wt::PlainText, container); + + artist->setInline(false); + track->setInline(false); + + artist->setStyleClass("playqueue-artist"); + track->setStyleClass("playqueue-track"); + + // Apply style if any + Wt::WString styleClass = Wt::asString(index.data(Wt::StyleClassRole)); + + // Apply selection style if any + if (flags & Wt::RenderSelected) + styleClass += " " + Wt::WApplication::instance()->theme()->activeClass(); + + container->setStyleClass(styleClass); + + res = container; } else res = Wt::WItemDelegate::update(widget, index, flags); @@ -269,14 +313,12 @@ _db(db), _curPlayedTrackPos(trackPosInvalid), _trackSelector(new TrackSelector()) { - _model = new Wt::WStandardItemModel(0, 5, this); + _model = new Wt::WStandardItemModel(0, 3, this); // 0 Column is hidden (track id) _model->setHeaderData(COLUMN_ID_TRACK_ID, Wt::WString("#")); - _model->setHeaderData(COLUMN_ID_POS, Wt::WString("#")); _model->setHeaderData(COLUMN_ID_COVER, Wt::WString("Cover")); _model->setHeaderData(COLUMN_ID_NAME, Wt::WString("Track")); - _model->setHeaderData(COLUMN_ID_DURATION, Wt::WString("Duration")); this->setModel(_model); this->setSelectionMode(Wt::ExtendedSelection); @@ -284,8 +326,9 @@ _trackSelector(new TrackSelector()) this->setAlternatingRowColors(true); this->setRowHeight(64); this->setColumnWidth(COLUMN_ID_COVER, 64); - this->setColumnWidth(COLUMN_ID_POS, 50); - this->setColumnWidth(COLUMN_ID_DURATION, 75); + this->setColumnWidth(COLUMN_ID_NAME, 240); + + this->setLayoutSizeAware(true); this->setColumnHidden(COLUMN_ID_TRACK_ID, true); @@ -310,6 +353,14 @@ _trackSelector(new TrackSelector()) } +void +PlayQueue::layoutSizeChanged (int width, int height) +{ + std::size_t coverColumnSize = this->columnWidth(COLUMN_ID_COVER).toPixels(); + // Set the remaining size for the name column + this->setColumnWidth(COLUMN_ID_NAME, width - coverColumnSize - (7 * 2) - 2); +} + void PlayQueue::setShuffle(bool enable) { @@ -358,10 +409,12 @@ PlayQueue::addTracks(const std::vector& trackIds) _model->insertRows(dataRow, 1); _model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id(), Wt::UserRole); - _model->setData(dataRow, COLUMN_ID_POS, dataRow + 1); _model->setData(dataRow, COLUMN_ID_COVER, track->hasCover() ? track->getPath() : "none", CoverRole); - _model->setData(dataRow, COLUMN_ID_NAME, track->getArtistName() + " - " + track->getName()); - _model->setData(dataRow, COLUMN_ID_DURATION, track->getDuration()); + + Name name; + name.track = Wt::WString::fromUTF8(track->getName()); + name.artist = Wt::WString::fromUTF8(track->getArtistName()); + _model->setData(dataRow, COLUMN_ID_NAME, name, NameRole); } } @@ -453,16 +506,12 @@ PlayQueue::setPlayingTrackPos(int newRowPos) // calling the update method of our custom item delegate gives bad results if (oldRowPos >= 0) { - _model->setData(oldRowPos, COLUMN_ID_POS, boost::any(), Wt::StyleClassRole); _model->setData(oldRowPos, COLUMN_ID_NAME, boost::any(), Wt::StyleClassRole); - _model->setData(oldRowPos, COLUMN_ID_DURATION, boost::any(), Wt::StyleClassRole); } if (newRowPos >= 0) { - _model->setData(newRowPos, COLUMN_ID_POS, "playqueue-playing", Wt::StyleClassRole); _model->setData(newRowPos, COLUMN_ID_NAME, "playqueue-playing", Wt::StyleClassRole); - _model->setData(newRowPos, COLUMN_ID_DURATION, "playqueue-playing", Wt::StyleClassRole); } } diff --git a/src/ui/audio/PlayQueue.hpp b/src/ui/audio/PlayQueue.hpp index 18b1d918..b82280b6 100644 --- a/src/ui/audio/PlayQueue.hpp +++ b/src/ui/audio/PlayQueue.hpp @@ -64,6 +64,8 @@ class PlayQueue : public Wt::WTableView private: + void layoutSizeChanged (int width, int height); + bool readTrack(int rowId); void setPlayingTrackPos(int newRowPos); void renumber(int firstId, int lastId); diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 1b5ed3f8..fb82490b 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -15,9 +15,28 @@ div.contents { } .playqueue-playing { + background-color: rgba(202, 167, 66, 1); +} + +.Wt-tableview .playqueue-playing { + color: #FFF; +} + +.Wt-tableview .Wt-tv-contents.Wt-striped div.playqueue-playing:nth-child(odd) { + background-color: rgba(202, 167, 66, 1); +} + +.playqueue-track { + margin-top: 12px; + line-height: normal; font-weight: bold; } +.playqueue-artist { + line-height: normal; + font-style: italic; +} + .mediaplayer { background-color: #ccc; border-radius: 10px; From 6b4a9ef38a9102a4b7e1ea28f8b3e9c5f58d7a97 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Dec 2014 13:49:26 +0100 Subject: [PATCH 2/3] [UI] Better delimited the play queue --- src/ui/audio/Audio.cpp | 13 ++++++++++++- src/ui/docroot/css/lms.css | 5 +++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ui/audio/Audio.cpp b/src/ui/audio/Audio.cpp index 766f880b..124dd1b2 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -74,9 +74,11 @@ _playQueue(nullptr) Wt::WHBoxLayout* trackControls = new Wt::WHBoxLayout(); Wt::WPushButton* playBtn = new Wt::WPushButton("Play"); + playBtn->setStyleClass("btn-sm"); trackControls->addWidget(playBtn); Wt::WPushButton* addBtn = new Wt::WPushButton("Add"); + addBtn->setStyleClass("btn-sm"); trackControls->addWidget(addBtn); trackControls->addWidget(new Wt::WText("Total duration: "), 1); @@ -94,7 +96,10 @@ _playQueue(nullptr) Wt::Dbo::Transaction transaction(_db.getSession()); Database::User::pointer user = _db.getCurrentUser(); + Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget(); + playQueueContainer->setStyleClass("playqueue"); Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); + playQueueContainer->setLayout(playQueueLayout); _mediaPlayer = new AudioMediaPlayer(); playQueueLayout->addWidget(_mediaPlayer); @@ -104,15 +109,21 @@ _playQueue(nullptr) Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout(); Wt::WPushButton *saveBtn = new Wt::WPushButton("Save"); + saveBtn->setStyleClass("btn-sm"); + playlistControls->addWidget(saveBtn); Wt::WPushButton *loadBtn = new Wt::WPushButton("Load"); + loadBtn->setStyleClass("btn-sm"); playlistControls->addWidget(loadBtn); Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); + upBtn->setStyleClass("btn-sm"); playlistControls->addWidget(upBtn); Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); + downBtn->setStyleClass("btn-sm"); playlistControls->addWidget(downBtn); Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); + delBtn->setStyleClass("btn-sm"); playlistControls->addWidget(delBtn); delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected); @@ -145,7 +156,7 @@ _playQueue(nullptr) playQueueLayout->addLayout(playlistControls); - mainLayout->addLayout(playQueueLayout, 0, 0, 2, 1); + mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1); } mainLayout->setRowStretch(1, 1); diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index fb82490b..f6333181 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -14,6 +14,11 @@ div.contents { padding: 0px 12px 6px; } +.playqueue { + background-color: #BBB; + border-radius: 10px; +} + .playqueue-playing { background-color: rgba(202, 167, 66, 1); } From 784b73fc5aad535f0075bafbb0440f7f18497211 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Dec 2014 15:31:13 +0100 Subject: [PATCH 3/3] [UI] Better handling covers: giving browsers a chance to properly cache them --- src/av/InputFormatContext.cpp | 33 +++++++++++++++---- src/av/InputFormatContext.hpp | 7 +++- src/cover/CoverArtGrabber.cpp | 6 ++-- src/ui/audio/PlayQueue.cpp | 52 +++++++---------------------- src/ui/audio/PlayQueue.hpp | 4 +++ src/ui/resource/CoverResource.cpp | 54 +++++++++++++++++++++++-------- src/ui/resource/CoverResource.hpp | 17 +++++----- 7 files changed, 100 insertions(+), 73 deletions(-) diff --git a/src/av/InputFormatContext.cpp b/src/av/InputFormatContext.cpp index 6fcdb432..2aab3f67 100644 --- a/src/av/InputFormatContext.cpp +++ b/src/av/InputFormatContext.cpp @@ -101,7 +101,7 @@ InputFormatContext::findStreamInfo(void) std::size_t InputFormatContext::getDurationSecs() const { - if (native()->duration != AV_NOPTS_VALUE ) + if (static_cast(native()->duration) != AV_NOPTS_VALUE ) return native()->duration / AV_TIME_BASE; else return 0; // TODO, do something better? @@ -128,18 +128,39 @@ InputFormatContext::getNbPictures(void) const } void -InputFormatContext::getPictures(std::vector< std::vector >& pictures) const +InputFormatContext::getPictures(std::vector& pictures) const { + static const std::map codecMimeMap = + { + { AV_CODEC_ID_BMP, "image/x-bmp" }, + { AV_CODEC_ID_GIF, "image/gif" }, + { AV_CODEC_ID_MJPEG, "image/jpeg" }, + { AV_CODEC_ID_PNG, "image/png" }, + { AV_CODEC_ID_PNG, "image/x-png" }, + { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, + }; + for (std::size_t i = 0; i < native()->nb_streams; ++i) { - if (native()->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) + Stream stream(native()->streams[i]); + + if (stream.hasAttachedPic()) { + Picture picture; + + auto itMime = codecMimeMap.find(stream.getCodecContext().getCodecId()); + if (itMime != codecMimeMap.end()) + picture.mimeType = itMime->second; + else + picture.mimeType = "application/octet-stream"; + + LMS_LOG(MOD_AV, SEV_DEBUG) << "MIME set to '" << picture.mimeType << "'" << std::endl; + AVPacket pkt = native()->streams[i]->attached_pic; - std::vector data; - std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(data)); + std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); - pictures.push_back( data ); + pictures.push_back( picture ); } } } diff --git a/src/av/InputFormatContext.hpp b/src/av/InputFormatContext.hpp index 91bb2725..7844ea97 100644 --- a/src/av/InputFormatContext.hpp +++ b/src/av/InputFormatContext.hpp @@ -31,6 +31,11 @@ namespace Av { +struct Picture { + std::string mimeType; + std::vector data; +}; + class InputFormatContext : public FormatContext { public: @@ -47,7 +52,7 @@ class InputFormatContext : public FormatContext // Get attached pictures std::size_t getNbPictures(void) const; - void getPictures(std::vector< std::vector >& pictures) const; + void getPictures(std::vector& pictures) const; // Get the streams std::vector getStreams(void); diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 8ca350ff..0a3fce62 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -37,11 +37,11 @@ Grabber::getFromInputFormatContext(const Av::InputFormatContext& input) try { - std::vector< std::vector > pictures; + std::vector pictures; input.getPictures(pictures); - BOOST_FOREACH(const std::vector& picture, pictures) - res.push_back( CoverArt("application/octet-stream", picture) ); + BOOST_FOREACH(const Av::Picture& picture, pictures) + res.push_back( CoverArt(picture.mimeType, picture.data) ); } catch(std::exception& e) diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 9c4beb4b..ed392bad 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -34,15 +34,12 @@ #include "PlayQueue.hpp" -static const int CoverRole = Wt::UserRole + 1; -static const int NameRole = Wt::UserRole + 2; +static const int NameRole = Wt::UserRole; namespace { void swapRows(Wt::WStandardItemModel *model, int row1, int row2) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "Swap called row1 = " << row1 << ", row2 = " << row2 << ", CoverRole = " << CoverRole; - // Swap data column by column for (int i = 0; i < model->columnCount(); ++i) { @@ -55,15 +52,7 @@ namespace { model->setItemData(index1, model->itemData(index2)); // 2 -> 1 model->setItemData(index2, tmp); // tmp-> 2 } - - // swap data associated with our custom roles - const std::vector(roles) = {CoverRole, NameRole}; - BOOST_FOREACH(int role, roles) - { - auto tmp = model->data(index1, role); - model->setData(index1, model->data(index2, role), role); - model->setData(index2, tmp, role); - } + // caution: swap data associated with our custom roles if any!! } } } @@ -245,33 +234,7 @@ class PlayQueueItemDelegate : public Wt::WItemDelegate { Wt::WWidget* res; - Wt::WString path = Wt::asString(index.data(CoverRole)); - if (!path.empty()) - { - // Create an image for this track - Wt::WImage* image = new Wt::WImage( ); - Wt::WResource* resource; - if (path != "none") - resource = new CoverResource(boost::filesystem::path(path.toUTF8()), 64, image); - else - resource = new Wt::WFileResource("image/jpeg", Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg", image); - - image->setImageLink(Wt::WLink(resource)); - - // Apply style if any - Wt::WString styleClass = Wt::asString(index.data(Wt::StyleClassRole)); - - // Apply selection style if any - if (flags & Wt::RenderSelected) - styleClass += " " + Wt::WApplication::instance()->theme()->activeClass(); - - image->setStyleClass(styleClass); - - res = image; - res->setObjectName("z"); - - } - else if (!index.data(NameRole).empty()) + if (!index.data(NameRole).empty()) { Name name = boost::any_cast(index.data(NameRole)); @@ -351,6 +314,7 @@ _trackSelector(new TrackSelector()) }, std::placeholders::_1, std::placeholders::_2)); + _coverResource = new CoverResource(db, 64); } void @@ -409,7 +373,13 @@ PlayQueue::addTracks(const std::vector& trackIds) _model->insertRows(dataRow, 1); _model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id(), Wt::UserRole); - _model->setData(dataRow, COLUMN_ID_COVER, track->hasCover() ? track->getPath() : "none", CoverRole); + + std::string coverUrl; + if (track->hasCover()) + coverUrl = _coverResource->url() + "&coverid=" + Wt::asString(track.id()).toUTF8(); + else + coverUrl = "images/unknown-cover.jpg"; + _model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole); Name name; name.track = Wt::WString::fromUTF8(track->getName()); diff --git a/src/ui/audio/PlayQueue.hpp b/src/ui/audio/PlayQueue.hpp index b82280b6..1f968353 100644 --- a/src/ui/audio/PlayQueue.hpp +++ b/src/ui/audio/PlayQueue.hpp @@ -26,6 +26,8 @@ #include "database/DatabaseHandler.hpp" #include "database/AudioTypes.hpp" +#include "resource/CoverResource.hpp" + namespace UserInterface { class PlayQueueItemDelegate; @@ -78,6 +80,8 @@ class PlayQueue : public Wt::WTableView PlayQueueItemDelegate* _itemDelegate; + CoverResource* _coverResource; + int _curPlayedTrackPos; std::unique_ptr _trackSelector; diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index d169278c..5af7ed2e 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -19,6 +19,7 @@ #include +#include #include #include "logger/Logger.hpp" @@ -29,9 +30,9 @@ namespace UserInterface { -CoverResource::CoverResource(const boost::filesystem::path& path, std::size_t size, Wt::WObject *parent) +CoverResource::CoverResource(Database::Handler& db, std::size_t size, Wt::WObject *parent) : Wt::WResource(parent), -_path(path), +_db(db), _size(size) { } @@ -45,25 +46,50 @@ CoverResource:: ~CoverResource() void CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - if (_data.empty()) + // Get the id of the track + const std::string *trackIdStr = request.getParameter("coverid"); + + if (trackIdStr) { - std::vector covers = CoverArt::Grabber::getFromTrack( _path ); + Database::Track::id_type trackId; + { + std::istringstream iss(*trackIdStr); iss >> trackId; + } + + Wt::Dbo::Transaction transaction(_db.getSession()); + + Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); + std::vector covers = CoverArt::Grabber::getFromTrack(track); + + transaction.commit(); BOOST_FOREACH(CoverArt::CoverArt& cover, covers) { - cover.scale(_size); - _data = cover.getData(); - _mimeType = cover.getMimeType(); - break; + if (cover.scale(_size)) + { + response.setMimeType( cover.getMimeType() ); + + BOOST_FOREACH(unsigned char c, cover.getData()) + response.out().put( c ); + + return; + } + else + LMS_LOG(MOD_UI, SEV_DEBUG) << "Resize error for track id = " << trackId; } + LMS_LOG(MOD_UI, SEV_DEBUG) << "no valid cover found for track id = " << trackId; + { + std::ifstream ist(Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"); - // TODO if not found fallback on an empty image + char c; + while(ist.get(c)) + response.out().put( c ); + + response.setMimeType("image/jpeg"); + } } - - response.setMimeType(_mimeType); - - for (unsigned int i = 0; i < _data.size(); ++i) - response.out().put(_data[i]); + else + LMS_LOG(MOD_UI, SEV_DEBUG) << "no cover id parameter"; } diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index a8554da9..fdc45277 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -17,11 +17,15 @@ * along with LMS. If not, see . */ +#ifndef COVER_RESOURCE_HPP_ +#define COVER_RESOURCE_HPP_ + #include #include #include +#include "database/DatabaseHandler.hpp" #include "database/AudioTypes.hpp" namespace UserInterface { @@ -29,8 +33,8 @@ namespace UserInterface { class CoverResource : public Wt::WResource { public: - CoverResource(const boost::filesystem::path& p, // track to get cover from - std::size_t size, // size * size pixels + CoverResource(Database::Handler& db, + std::size_t size, // size * size pixels Wt::WObject *parent = 0); ~CoverResource(); @@ -38,13 +42,10 @@ class CoverResource : public Wt::WResource private: - boost::filesystem::path _path; + Database::Handler& _db; std::size_t _size; - std::string _mimeType; - - std::vector _data; }; - - } // namespace UserInterface + +#endif