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/av/InputFormatContext.cpp b/src/av/InputFormatContext.cpp index 346702fe..6fcdb432 100644 --- a/src/av/InputFormatContext.cpp +++ b/src/av/InputFormatContext.cpp @@ -114,6 +114,19 @@ InputFormatContext::getMetadata(void) } +std::size_t +InputFormatContext::getNbPictures(void) const +{ + std::size_t res = 0; + + for (std::size_t i = 0; i < native()->nb_streams; ++i) + { + if (native()->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) + ++res; + } + return res; +} + void InputFormatContext::getPictures(std::vector< std::vector >& pictures) const { diff --git a/src/av/InputFormatContext.hpp b/src/av/InputFormatContext.hpp index 11fe1c89..91bb2725 100644 --- a/src/av/InputFormatContext.hpp +++ b/src/av/InputFormatContext.hpp @@ -46,6 +46,7 @@ class InputFormatContext : public FormatContext // Get attached pictures + std::size_t getNbPictures(void) const; void getPictures(std::vector< std::vector >& pictures) const; // Get the streams 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/database-updater/DatabaseUpdater.cpp b/src/database-updater/DatabaseUpdater.cpp index 22b7e530..37c1c196 100644 --- a/src/database-updater/DatabaseUpdater.cpp +++ b/src/database-updater/DatabaseUpdater.cpp @@ -402,6 +402,11 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) track.modify()->setDate( boost::any_cast(items[MetaData::Type::OriginalDate]) ); } + if (items.find(MetaData::Type::HasCover) != items.end()) + { + track.modify()->setHasCover( boost::any_cast(items[MetaData::Type::HasCover])); + } + transaction.commit(); } diff --git a/src/database/AudioTypes.hpp b/src/database/AudioTypes.hpp index ddc21bf6..08ccc1d5 100644 --- a/src/database/AudioTypes.hpp +++ b/src/database/AudioTypes.hpp @@ -117,6 +117,7 @@ class Track void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; } void setGenres(const std::string& genreList) { _genreList = genreList; } void setGenres(std::vector genres); + void setHasCover(bool hasCover) { _hasCover = hasCover; } int getTrackNumber(void) const { return _trackNumber; } int getDiscNumber(void) const { return _discNumber; } @@ -132,6 +133,7 @@ class Track boost::posix_time::ptime getLastWriteTime(void) const { return _fileLastWrite; } const std::vector& getChecksum(void) const { return _fileChecksum; } + bool hasCover(void) const { return _hasCover; } template void persist(Action& a) @@ -148,6 +150,7 @@ class Track Wt::Dbo::field(a, _filePath, "path"); Wt::Dbo::field(a, _fileLastWrite, "last_write"); Wt::Dbo::field(a, _fileChecksum, "checksum"); + Wt::Dbo::field(a, _hasCover, "has_cover"); Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade); } @@ -167,6 +170,7 @@ class Track std::string _filePath; std::vector _fileChecksum; boost::posix_time::ptime _fileLastWrite; + bool _hasCover; Wt::Dbo::collection< Genre::pointer > _genres; // Tracks that belong to this genre }; diff --git a/src/database/Track.cpp b/src/database/Track.cpp index d0e966cb..f2df60e2 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -29,7 +29,8 @@ Track::Track(const boost::filesystem::path& p) : _trackNumber(0), _discNumber(0), -_filePath( p.string() ) +_filePath( p.string() ), +_hasCover(false) { } diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp index d7d9d87f..821d6191 100644 --- a/src/metadata/AvFormat.cpp +++ b/src/metadata/AvFormat.cpp @@ -114,6 +114,9 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items) // Duration items.insert( std::make_pair(MetaData::Type::Duration, boost::posix_time::time_duration( boost::posix_time::seconds( input.getDurationSecs() )) )); + // Cover + items.insert( std::make_pair(MetaData::Type::HasCover, input.getNbPictures() > 0)); + // Embedded MetaData // Make sure to convert strings into UTF-8 std::map::const_iterator it; diff --git a/src/metadata/MetaData.hpp b/src/metadata/MetaData.hpp index 93fc1154..07dec04d 100644 --- a/src/metadata/MetaData.hpp +++ b/src/metadata/MetaData.hpp @@ -39,18 +39,12 @@ namespace MetaData DiscNumber, // size_t Date, // boost::posix_time::ptime OriginalDate, // boost::posix_time::ptime - Cover, // GenericData + HasCover, // bool AudioStreams, // vector VideoStreams, // vector SubtitleStreams, // vector }; - // Used by Cover - struct GenericData { - std::string mimeType; - std::vector data; - }; - // Used by Streams struct AudioStream { std::size_t nbChannels; diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index a4328edc..ad803ae6 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -92,25 +92,35 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, boost::filesystem::p // If here is no account in the database, launch the first connection wizard if (firstConnection) - { - // Hack, use the auth widget builtin strings - builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1); - - root()->addWidget( new Settings::FirstConnectionFormView(_sessionData)); - } + createFirstConnectionUI(); else - { - _sessionData.getDatabaseHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent); + createLmsUI(); - LmsAuth *authWidget = new LmsAuth(_sessionData.getDatabaseHandler()); +} - authWidget->model()->addPasswordAuth(&Database::Handler::getPasswordService()); - authWidget->setRegistrationEnabled(false); +void +LmsApplication::createFirstConnectionUI() +{ + // Hack, use the auth widget builtin strings + builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1); - authWidget->processEnvironment(); + root()->addWidget( new Settings::FirstConnectionFormView(_sessionData)); +} - root()->addWidget(authWidget); - } +void +LmsApplication::createLmsUI() +{ + + _sessionData.getDatabaseHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent); + + LmsAuth *authWidget = new LmsAuth(_sessionData.getDatabaseHandler()); + + authWidget->model()->addPasswordAuth(&Database::Handler::getPasswordService()); + authWidget->setRegistrationEnabled(false); + + authWidget->processEnvironment(); + + root()->addWidget(authWidget); } diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index 6d4b1259..f1f814d0 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -38,6 +38,8 @@ class LmsApplication : public Wt::WApplication private: void handleAuthEvent(void); + void createFirstConnectionUI(); + void createLmsUI(); SessionData _sessionData; diff --git a/src/ui/approot/templates.xml b/src/ui/approot/templates.xml index f4feb002..4a635a85 100644 --- a/src/ui/approot/templates.xml +++ b/src/ui/approot/templates.xml @@ -308,6 +308,16 @@ ${apply-info} + + +
+ ${prev} + ${play} + ${pause} + ${next} +
+
+ diff --git a/src/ui/audio/Audio.cpp b/src/ui/audio/Audio.cpp index b26eb648..766f880b 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); @@ -96,6 +96,11 @@ _playQueue(nullptr) Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); + _mediaPlayer = new AudioMediaPlayer(); + playQueueLayout->addWidget(_mediaPlayer); + + playQueueLayout->addWidget( _playQueue, 1); + Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout(); Wt::WPushButton *saveBtn = new Wt::WPushButton("Save"); @@ -140,17 +145,12 @@ _playQueue(nullptr) playQueueLayout->addLayout(playlistControls); - playQueueLayout->addWidget( _playQueue, 1); - - mainLayout->addLayout(playQueueLayout, 0, 1, 2, 1); + mainLayout->addLayout(playQueueLayout, 0, 0, 2, 1); } - _mediaPlayer = new AudioMediaPlayer(); - mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 2); - 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/AudioMediaPlayer.cpp b/src/ui/audio/AudioMediaPlayer.cpp index c3308062..610c59f2 100644 --- a/src/ui/audio/AudioMediaPlayer.cpp +++ b/src/ui/audio/AudioMediaPlayer.cpp @@ -17,10 +17,12 @@ * along with LMS. If not, see . */ - #include #include #include +#include +#include +#include #include "AudioMediaPlayer.hpp" @@ -30,65 +32,94 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent), _mediaResource(nullptr) { - _mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, this ); -// _mediaPlayer->setAlternativeContent (new Wt::WText("You don't have HTML5 audio support!")); -// _mediaPlayer->setOptions( Wt::WMediaPlayer::Autoplay ); - _mediaPlayer->addSource( Wt::WMediaPlayer::OGA, "" ); + this->setStyleClass("mediaplayer"); + this->setHeight(100); + Wt::WContainerWidget *sliderContainer = new Wt::WContainerWidget(this); + Wt::WHBoxLayout *sliderLayout = new Wt::WHBoxLayout(); + sliderContainer->setLayout(sliderLayout); + + sliderLayout->addWidget(_curTime = new Wt::WText("00:00:00")); + _curTime->setLineHeight(30); + sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1); + _timeSlider->setHeight(30); + sliderLayout->addWidget(_duration = new Wt::WText("00:00:00")); + _duration->setLineHeight(30); + + Wt::WContainerWidget *controlsContainer = new Wt::WContainerWidget(this); + Wt::WHBoxLayout *controlsLayout = new Wt::WHBoxLayout(); + controlsContainer->setLayout(controlsLayout); + + Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget(); + + Wt::WTemplate *t = new Wt::WTemplate(Wt::WString::tr("mediaplayer-controls"), btnContainer); + + Wt::WPushButton *prevBtn = new Wt::WPushButton("<<"); + t->bindWidget("prev", prevBtn); + + _playBtn = new Wt::WPushButton("Play"); + t->bindWidget("play", _playBtn); + _playBtn->setWidth(70); + + _pauseBtn = new Wt::WPushButton("Pause"); + t->bindWidget("pause", _pauseBtn); + _pauseBtn->setWidth(70); + + Wt::WPushButton *nextBtn = new Wt::WPushButton(">>"); + t->bindWidget("next", nextBtn); + + controlsLayout->addWidget(btnContainer); + + _volumeSlider = new Wt::WSlider(); + _volumeSlider->setRange(0,100); + _volumeSlider->setWidth(60); + _volumeSlider->setMinimumSize(60, Wt::WLength::Auto); + _volumeSlider->setHeight(30); + controlsLayout->addWidget(_volumeSlider); + +/* Wt::WText* name = new Wt::WText("My track name"); + controlsLayout->addWidget(name, 1);*/ + + Wt::WCheckBox *loop = new Wt::WCheckBox("Loop", controlsContainer); + loop->changed().connect(std::bind([=] () + { + _loop.emit( loop->checkState() == Wt::Checked); + })); + + Wt::WCheckBox *shuffle = new Wt::WCheckBox("Shuffle", controlsContainer); + shuffle->changed().connect(std::bind([=] () + { + _shuffle.emit( shuffle->checkState() == Wt::Checked); + })); + + controlsLayout->addWidget(loop); + controlsLayout->addWidget(shuffle); + + _mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, this ); + _mediaPlayer->addSource( Wt::WMediaPlayer::OGA, "" ); _mediaPlayer->ended().connect(this, &AudioMediaPlayer::handleTrackEnded); - { - Wt::WContainerWidget *container = new Wt::WContainerWidget(this); + _mediaPlayer->setControlsWidget( 0 ); + _mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn); + _mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn); - Wt::WCheckBox *loop = new Wt::WCheckBox("Loop", this); - loop->changed().connect(std::bind([=] () - { - _loop.emit( loop->checkState() == Wt::Checked); - })); + _mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated); - Wt::WCheckBox *shuffle = new Wt::WCheckBox("Shuffle", this); - shuffle->changed().connect(std::bind([=] () - { - _shuffle.emit( shuffle->checkState() == Wt::Checked); - })); + _volumeSlider->setValue(_mediaPlayer->volume() * 100); - Wt::WPushButton *prevBtn = new Wt::WPushButton("<<", container ); - _playBtn = new Wt::WPushButton("Play", container ); - _pauseBtn = new Wt::WPushButton("Pause", container ); - Wt::WPushButton *nextBtn = new Wt::WPushButton(">>", container ); + nextBtn->clicked().connect(std::bind([=] () + { + _mediaPlayer->stop(); + _playNext.emit(); + })); - _curTime = new Wt::WText(container); - _timeSlider = new Wt::WSlider( container ); - _duration = new Wt::WText(container); - - _volumeSlider = new Wt::WSlider( container ); - _volumeSlider->setRange(0,100); - _volumeSlider->setValue(_mediaPlayer->volume() * 100); - - _mediaPlayer->setControlsWidget( container ); - _mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn); - _mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn); - - _mediaPlayer->setText( Wt::WMediaPlayer::CurrentTime, _curTime); - _mediaPlayer->setText( Wt::WMediaPlayer::Duration, _duration); - - _mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated); - - nextBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playNext.emit(); - })); - - prevBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playPrevious.emit(); - })); - } - - _timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset); - _timeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleSliderMoved); + prevBtn->clicked().connect(std::bind([=] () + { + _mediaPlayer->stop(); + _playPrevious.emit(); + })); + _timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset); +// _timeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleSliderMoved); _timeSlider->setDisabled(true); _volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved); diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 2183b3c1..3e17d9ef 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -20,35 +20,60 @@ #include #include +#include +#include +#include #include +#include +#include + +#include "resource/CoverResource.hpp" #include "logger/Logger.hpp" #include "PlayQueue.hpp" -namespace { +static const int CoverRole = Wt::UserRole + 1; - void modelForceRefreshDataRow(Wt::WStandardItemModel *model, int row) - { - for (int i = 0; i < model->columnCount(); ++i) - model->setData(row, i, model->data(row, i)); - } +namespace { void swapRows(Wt::WStandardItemModel *model, int row1, int row2) { - // Copy column by column + 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) { - boost::any tmp = model->data(row1, i); // 1 -> tmp - model->setData(row1, i, model->data(row2, i)); // 2 -> 1 - model->setData(row2, i, tmp); // tmp-> 2 + Wt::WModelIndex index1 = model->index(row1, i); + Wt::WModelIndex index2 = model->index(row2, i); + + { + 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); + } } } - } 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 +}; + static const int trackPosInvalid = -1; class TrackSelector @@ -59,8 +84,8 @@ class TrackSelector void setShuffle(bool enable); void setLoop(bool enable) { _loop = enable; } - int getPrevious(void); - int getNext(void); + int previous(void); + int next(void); int getCurrent(void); // Set the internal pos thanks to the track pos @@ -86,11 +111,12 @@ class TrackSelector TrackSelector::TrackSelector() : _loop(false), _shuffle(false), +_size(0), _curPos(0) { } - void +void TrackSelector::refreshPositions() { _trackPos.clear(); @@ -102,7 +128,7 @@ TrackSelector::refreshPositions() _trackPos.push_back(i); // Now shuffle - // Perform size/2 permutations + // Source: http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle boost::random::mt19937 rng; // produces randomness out of thin air rng.seed(static_cast(std::time(0))); @@ -127,14 +153,18 @@ TrackSelector::setShuffle(bool enable) { _shuffle = enable; - if (_shuffle) - setPosByRowId(_curPos); - else - setPos(_trackPos[_curPos]); + if (_size != 0) + { + // Update the current index we are playing if we switch shuffle + if (_shuffle) + setPosByRowId(_curPos); + else + setPos(_trackPos[_curPos]); + } } int -TrackSelector::getNext() +TrackSelector::next() { if (_size == 0) return trackPosInvalid; @@ -153,7 +183,7 @@ TrackSelector::getNext() } int -TrackSelector::getPrevious() +TrackSelector::previous() { if (_size == 0) return trackPosInvalid; @@ -200,55 +230,64 @@ 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); + // 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 - res->toggleStyleClass("playqueue-playing", false); + resource = new Wt::WFileResource("image/jpeg", Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg", 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 +357,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(), 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()); } } @@ -351,7 +391,7 @@ PlayQueue::playNext(void) while (nbTries > 0) { - int pos = _trackSelector->getNext(); + int pos = _trackSelector->next(); if (pos == trackPosInvalid) break; @@ -369,7 +409,7 @@ PlayQueue::playPrevious(void) while (nbTries > 0) { - int pos = _trackSelector->getPrevious(); + int pos = _trackSelector->previous(); if (pos == trackPosInvalid) break; @@ -386,7 +426,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, Wt::UserRole)); Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); if (track) @@ -395,6 +435,8 @@ PlayQueue::readTrack(int rowPos) _sigTrackPlay.emit(track->getPath()); + this->scrollTo( _model->index(_trackSelector->getCurrent(), 0)); + return true; } else @@ -407,15 +449,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 +480,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/audio/TableFilter.cpp b/src/ui/audio/TableFilter.cpp index 4c5fbf6d..b2c156db 100644 --- a/src/ui/audio/TableFilter.cpp +++ b/src/ui/audio/TableFilter.cpp @@ -41,6 +41,8 @@ _field(field) this->setAlternatingRowColors(true); this->setModel(&_queryModel); + this->setColumnWidth(1, 50); + this->selectionChanged().connect(this, &TableFilter::emitUpdate); setLayoutSizeAware(true); @@ -66,14 +68,15 @@ _field(field) void TableFilter::layoutSizeChanged (int width, int height) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "LAYOUT CHANGED!"; + LMS_LOG(MOD_UI, SEV_DEBUG) << "LAYOUT CHANGED, new width = " << width; - /* TODO - std::size_t trackColumnSize = this->columnWidth(1).toPixels() + 30 ; + LMS_LOG(MOD_UI, SEV_DEBUG) << "Before: sizes = " << this->columnWidth(0).toPixels() << ", " << this->columnWidth(1).toPixels(); + + std::size_t trackColumnSize = this->columnWidth(1).toPixels(); // Set the remaining size for the name column - this->setColumnWidth(0, width - 7 - trackColumnSize); - */ + this->setColumnWidth(0, width - trackColumnSize - (7 * 2) - 2); + LMS_LOG(MOD_UI, SEV_DEBUG) << "After: sizes = " << this->columnWidth(0).toPixels() << ", " << this->columnWidth(1).toPixels(); } // Set constraints on this filter diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 48f18b91..1b5ed3f8 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -1,5 +1,25 @@ - -.playqueue-playing { - font-weight: bold; +.main-nav { + margin-bottom: 0px; +} + +.Wt-hrh2 { + background-color: white; +} + +.Wt-vrh2 { + background-color: white; +} + +div.contents { + padding: 0px 12px 6px; +} + +.playqueue-playing { + font-weight: bold; +} + +.mediaplayer { + background-color: #ccc; + border-radius: 10px; } diff --git a/src/ui/docroot/images/unknown-cover.jpg b/src/ui/docroot/images/unknown-cover.jpg new file mode 100755 index 00000000..19ffc706 Binary files /dev/null and b/src/ui/docroot/images/unknown-cover.jpg differ 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 diff --git a/src/ui/settings/Settings.cpp b/src/ui/settings/Settings.cpp index 2f8d4515..4ccc57e1 100644 --- a/src/ui/settings/Settings.cpp +++ b/src/ui/settings/Settings.cpp @@ -51,6 +51,7 @@ _sessionData(sessionData) contents->setStyleClass("contents"); contents->setOverflow(WContainerWidget::OverflowHidden); + // TODO menu style on hover Wt::WMenu *menu = new Wt::WMenu(contents, Wt::Vertical); menu->setStyleClass("nav nav-pills nav-stacked submenu"); menu->setWidth(150);