diff --git a/src/Makefile.am b/src/Makefile.am index ca1124bc..6912e71a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -51,6 +51,7 @@ lms_SOURCES = \ $(srcdir)/ui/common/DirectoryValidator.cpp \ $(srcdir)/ui/common/SessionData.cpp \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ + $(srcdir)/ui/resource/CoverResource.cpp \ $(srcdir)/ui/video/VideoWidget.cpp \ $(srcdir)/ui/video/VideoDatabaseWidget.cpp \ $(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \ diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 7dabf4c5..dccfaf77 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -52,6 +52,25 @@ Grabber::getFromInputFormatContext(const Av::InputFormatContext& input) return res; } +std::vector +Grabber::getFromTrack(const boost::filesystem::path& p) +{ + std::vector res; + + try + { + Av::InputFormatContext input(p); + + return getFromInputFormatContext(input); + + } + catch(std::exception& e) + { + LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get pictures: " << e.what(); + } + + return res; +} diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index d9ce3a82..757356e7 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -36,6 +36,7 @@ class Grabber static std::vector getFromInputFormatContext(const Av::InputFormatContext& input); static std::vector getFromTrack(Database::Track::pointer track); + static std::vector getFromTrack(const boost::filesystem::path& path); }; } // namespace CoverArt diff --git a/src/ui/audio/Audio.cpp b/src/ui/audio/Audio.cpp index b26eb648..abcaa61d 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -61,7 +61,7 @@ _playQueue(nullptr) filterLayout->addWidget(filterRelease); _filterChain.addFilter(filterRelease); - mainLayout->addLayout(filterLayout, 0, 0); + mainLayout->addLayout(filterLayout, 0, 1); // ENDOF(Filters) // TODO ADD some stats (nb files, total duration, etc.) @@ -83,7 +83,7 @@ _playQueue(nullptr) trackLayout->addLayout(trackControls); - mainLayout->addLayout(trackLayout, 1, 0); + mainLayout->addLayout(trackLayout, 1, 1); _filterChain.addFilter(_trackView); @@ -142,15 +142,15 @@ _playQueue(nullptr) playQueueLayout->addWidget( _playQueue, 1); - mainLayout->addLayout(playQueueLayout, 0, 1, 2, 1); - } + _mediaPlayer = new AudioMediaPlayer(); + playQueueLayout->addWidget(_mediaPlayer); - _mediaPlayer = new AudioMediaPlayer(); - mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 2); + mainLayout->addLayout(playQueueLayout, 0, 0, 2, 1); + } mainLayout->setRowStretch(1, 1); mainLayout->setRowResizable(0, true, Wt::WLength(200, Wt::WLength::Pixel)); - mainLayout->setColumnResizable(0, true); + mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel)); // Double click on track // Set the selected tracks to the play queue diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 2183b3c1..ab1eb83f 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -20,7 +20,12 @@ #include #include +#include +#include #include +#include + +#include "resource/CoverResource.hpp" #include "logger/Logger.hpp" @@ -28,12 +33,6 @@ namespace { - void modelForceRefreshDataRow(Wt::WStandardItemModel *model, int row) - { - for (int i = 0; i < model->columnCount(); ++i) - model->setData(row, i, model->data(row, i)); - } - void swapRows(Wt::WStandardItemModel *model, int row1, int row2) { // Copy column by column @@ -44,11 +43,21 @@ namespace { model->setData(row2, i, tmp); // tmp-> 2 } } - } namespace UserInterface { +static const int CoverRole = Wt::UserRole + 1; + +enum ColumnId +{ + COLUMN_ID_TRACK_ID = 0, + COLUMN_ID_POS = 1, + COLUMN_ID_COVER = 2, + COLUMN_ID_NAME = 3, + COLUMN_ID_DURATION = 4 +}; + static const int trackPosInvalid = -1; class TrackSelector @@ -90,7 +99,7 @@ _curPos(0) { } - void +void TrackSelector::refreshPositions() { _trackPos.clear(); @@ -200,55 +209,59 @@ TrackSelector::setSize(std::size_t size) class PlayQueueItemDelegate : public Wt::WItemDelegate { public: - PlayQueueItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent), _selectedRowPos(trackPosInvalid) {} - - void setSelectedRowPos(int rowPos) { _selectedRowPos = rowPos; } + PlayQueueItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent) {} Wt::WWidget* update(Wt::WWidget *widget, const Wt::WModelIndex &index, Wt::WFlags< Wt::ViewItemRenderFlag > flags) { - Wt::WWidget* res = Wt::WItemDelegate::update(widget, index, flags); + Wt::WWidget* res; - if (res && index.isValid()) + Wt::WString path = Wt::asString(index.data(CoverRole)); + if (!path.empty()) { - if (_selectedRowPos != trackPosInvalid && index.row() == _selectedRowPos) - res->toggleStyleClass("playqueue-playing", true); - else - res->toggleStyleClass("playqueue-playing", false); + // Create an image for this track + Wt::WImage *image = new Wt::WImage( ); + CoverResource *resource = new CoverResource(boost::filesystem::path(path.toUTF8()), 64, image); + image->setImageLink(Wt::WLink(resource)); + + res = image; + res->setObjectName("z"); } + else + res = Wt::WItemDelegate::update(widget, index, flags); return res; } private: - int _selectedRowPos; }; - PlayQueue::PlayQueue(Database::Handler& db, Wt::WContainerWidget* parent) : Wt::WTableView(parent), _db(db), _curPlayedTrackPos(trackPosInvalid), _trackSelector(new TrackSelector()) { - _model = new Wt::WStandardItemModel(0, 4, this); + _model = new Wt::WStandardItemModel(0, 5, this); // 0 Column is hidden (track id) - _model->setHeaderData(0, Wt::WString("#")); - _model->setHeaderData(1, Wt::WString("#")); - _model->setHeaderData(2, Wt::WString("Track")); - _model->setHeaderData(3, Wt::WString("Duration")); + _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); this->setSortingEnabled(false); 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(0, 50); - this->setColumnWidth(1, 50); - - this->setColumnHidden(0, true); + this->setColumnHidden(COLUMN_ID_TRACK_ID, true); _itemDelegate = new PlayQueueItemDelegate(); this->setItemDelegate(_itemDelegate); @@ -318,10 +331,11 @@ PlayQueue::addTracks(const std::vector& trackIds) _model->insertRows(dataRow, 1); - _model->setData(dataRow, 0, track.id()); - _model->setData(dataRow, 1, dataRow + 1); - _model->setData(dataRow, 2, track->getArtistName() + " - " + track->getName()); - _model->setData(dataRow, 3, track->getDuration()); + _model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id()); + _model->setData(dataRow, COLUMN_ID_POS, dataRow + 1); + _model->setData(dataRow, COLUMN_ID_COVER, track->getPath(), CoverRole); + _model->setData(dataRow, COLUMN_ID_NAME, track->getArtistName() + " - " + track->getName()); + _model->setData(dataRow, COLUMN_ID_DURATION, track->getDuration()); } } @@ -386,7 +400,7 @@ PlayQueue::readTrack(int rowPos) Wt::Dbo::Transaction transaction(_db.getSession()); LMS_LOG(MOD_UI, SEV_DEBUG) << "Reading track at pos " << rowPos; - Database::Track::id_type trackId = boost::any_cast(_model->data(rowPos, 0)); + Database::Track::id_type trackId = boost::any_cast(_model->data(rowPos, COLUMN_ID_TRACK_ID)); Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); if (track) @@ -407,15 +421,21 @@ PlayQueue::setPlayingTrackPos(int newRowPos) int oldRowPos = _curPlayedTrackPos; _curPlayedTrackPos = newRowPos; - _itemDelegate->setSelectedRowPos(newRowPos); - // Hack re-set the data in order to trigger the rerending of the widget // calling the update method of our custom item delegate gives bad results if (oldRowPos >= 0) - modelForceRefreshDataRow(_model, oldRowPos); + { + _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) - modelForceRefreshDataRow(_model, newRowPos); + { + _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); + } } void @@ -432,7 +452,6 @@ PlayQueue::delSelected(void) } // If the current played track is removed, make sure to unselect it - _itemDelegate->setSelectedRowPos(trackPosInvalid); _trackSelector->setSize(_model->rowCount()); renumber(minId, _model->rowCount() - 1); diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp new file mode 100644 index 00000000..d169278c --- /dev/null +++ b/src/ui/resource/CoverResource.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 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 . + */ + +#include + +#include + +#include "logger/Logger.hpp" + +#include "cover/CoverArtGrabber.hpp" + +#include "CoverResource.hpp" + +namespace UserInterface { + +CoverResource::CoverResource(const boost::filesystem::path& path, std::size_t size, Wt::WObject *parent) +: Wt::WResource(parent), +_path(path), +_size(size) +{ +} + +CoverResource:: ~CoverResource() +{ + beingDeleted(); +} + + +void +CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) +{ + if (_data.empty()) + { + std::vector covers = CoverArt::Grabber::getFromTrack( _path ); + + BOOST_FOREACH(CoverArt::CoverArt& cover, covers) + { + cover.scale(_size); + _data = cover.getData(); + _mimeType = cover.getMimeType(); + break; + } + + // TODO if not found fallback on an empty image + } + + response.setMimeType(_mimeType); + + for (unsigned int i = 0; i < _data.size(); ++i) + response.out().put(_data[i]); +} + + +} // namespace UserInterface diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp new file mode 100644 index 00000000..a8554da9 --- /dev/null +++ b/src/ui/resource/CoverResource.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 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 . + */ + +#include +#include + +#include + +#include "database/AudioTypes.hpp" + +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 + Wt::WObject *parent = 0); + ~CoverResource(); + + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); + + private: + + boost::filesystem::path _path; + std::size_t _size; + std::string _mimeType; + + std::vector _data; +}; + + + +} // namespace UserInterface