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/Audio.cpp b/src/ui/audio/Audio.cpp index ec8f8412..32274ac3 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -73,9 +73,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); @@ -93,8 +95,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(); - playQueueLayout->setContentsMargins(5,5,5,5); + playQueueContainer->setLayout(playQueueLayout); _mediaPlayer = new AudioMediaPlayer(); playQueueLayout->addWidget(_mediaPlayer); @@ -104,15 +108,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 +155,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/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 3e17d9ef..ed392bad 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" @@ -33,32 +34,25 @@ #include "PlayQueue.hpp" -static const int CoverRole = Wt::UserRole + 1; +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) { 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 - { - auto tmp = model->data(index1, CoverRole); - model->setData(index1, model->data(index2, CoverRole), CoverRole); - model->setData(index2, tmp, CoverRole); - } + // caution: swap data associated with our custom roles if any!! } } } @@ -68,10 +62,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 +219,12 @@ TrackSelector::setSize(std::size_t size) refreshPositions(); } +struct Name +{ + Wt::WString track; + Wt::WString artist; +}; + class PlayQueueItemDelegate : public Wt::WItemDelegate { public: @@ -236,21 +234,30 @@ class PlayQueueItemDelegate : public Wt::WItemDelegate { Wt::WWidget* res; - Wt::WString path = Wt::asString(index.data(CoverRole)); - if (!path.empty()) + if (!index.data(NameRole).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); + Name name = boost::any_cast(index.data(NameRole)); - image->setImageLink(Wt::WLink(resource)); + 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); - res = image; - res->setObjectName("z"); + 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 +276,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 +289,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); @@ -308,6 +314,15 @@ _trackSelector(new TrackSelector()) }, std::placeholders::_1, std::placeholders::_2)); + _coverResource = new CoverResource(db, 64); +} + +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 @@ -358,10 +373,18 @@ 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()); + + 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()); + name.artist = Wt::WString::fromUTF8(track->getArtistName()); + _model->setData(dataRow, COLUMN_ID_NAME, name, NameRole); } } @@ -453,16 +476,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..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; @@ -64,6 +66,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); @@ -76,6 +80,8 @@ class PlayQueue : public Wt::WTableView PlayQueueItemDelegate* _itemDelegate; + CoverResource* _coverResource; + int _curPlayedTrackPos; std::unique_ptr _trackSelector; diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 1b5ed3f8..f6333181 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -14,10 +14,34 @@ div.contents { padding: 0px 12px 6px; } +.playqueue { + background-color: #BBB; + border-radius: 10px; +} + .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; 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