From b35ed6afca19c2968fd9e3be6bf4a0b5fbdffe08 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Dec 2014 19:33:04 +0100 Subject: [PATCH 01/11] [UI] Corrected memleaks --- src/ui/settings/SettingsAccountFormView.cpp | 1 - src/ui/settings/SettingsAudioFormView.cpp | 2 +- src/ui/settings/SettingsUserFormView.cpp | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ui/settings/SettingsAccountFormView.cpp b/src/ui/settings/SettingsAccountFormView.cpp index 3919950a..1c87496e 100644 --- a/src/ui/settings/SettingsAccountFormView.cpp +++ b/src/ui/settings/SettingsAccountFormView.cpp @@ -19,7 +19,6 @@ #include -#include #include #include #include diff --git a/src/ui/settings/SettingsAudioFormView.cpp b/src/ui/settings/SettingsAudioFormView.cpp index 6116991b..806e2884 100644 --- a/src/ui/settings/SettingsAudioFormView.cpp +++ b/src/ui/settings/SettingsAudioFormView.cpp @@ -99,7 +99,7 @@ class AudioFormModel : public Wt::WFormModel Database::User::pointer user = Database::User::getById(_db.getSession(), _userId); - _bitrateModel = new Wt::WStringListModel(); + _bitrateModel = new Wt::WStringListModel(this); BOOST_FOREACH(std::size_t bitrate, Database::User::audioBitrates) { if (user && bitrate <= user->getMaxAudioBitrate()) diff --git a/src/ui/settings/SettingsUserFormView.cpp b/src/ui/settings/SettingsUserFormView.cpp index 5205d026..d56f054c 100644 --- a/src/ui/settings/SettingsUserFormView.cpp +++ b/src/ui/settings/SettingsUserFormView.cpp @@ -269,12 +269,12 @@ class UserFormModel : public Wt::WFormModel void initializeModels() { // AUDIO - _audioBitrateModel = new Wt::WStringListModel(); + _audioBitrateModel = new Wt::WStringListModel(this); BOOST_FOREACH(std::size_t bitrate, Database::User::audioBitrates) _audioBitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps // VIDEO - _videoBitrateModel = new Wt::WStringListModel(); + _videoBitrateModel = new Wt::WStringListModel(this); BOOST_FOREACH(std::size_t bitrate, Database::User::videoBitrates) _videoBitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps From 3f368f17e456e884c25d8c9b87f0c58d2ce5213d Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 7 Dec 2014 19:43:22 +0100 Subject: [PATCH 02/11] [UI] PlayQueue on the left, added covers for each entry --- src/Makefile.am | 1 + src/cover/CoverArtGrabber.cpp | 19 +++++++ src/cover/CoverArtGrabber.hpp | 1 + src/ui/audio/Audio.cpp | 14 ++--- src/ui/audio/PlayQueue.cpp | 95 ++++++++++++++++++------------- src/ui/resource/CoverResource.cpp | 70 +++++++++++++++++++++++ src/ui/resource/CoverResource.hpp | 50 ++++++++++++++++ 7 files changed, 205 insertions(+), 45 deletions(-) create mode 100644 src/ui/resource/CoverResource.cpp create mode 100644 src/ui/resource/CoverResource.hpp 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 From a341c3e50849b18812e7e9ca82f594ae98666683 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 9 Dec 2014 12:38:47 +0100 Subject: [PATCH 03/11] [UI] Workaround to make covers move in the playqueue --- src/ui/audio/PlayQueue.cpp | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index ab1eb83f..3e13f5cc 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -31,24 +31,38 @@ #include "PlayQueue.hpp" +static const int CoverRole = Wt::UserRole + 1; + 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 { -static const int CoverRole = Wt::UserRole + 1; - enum ColumnId { COLUMN_ID_TRACK_ID = 0, @@ -215,9 +229,13 @@ class PlayQueueItemDelegate : public Wt::WItemDelegate { Wt::WWidget* res; + LMS_LOG(MOD_UI, SEV_DEBUG) << "Update called! widget = " << widget << ", row = " << index.row() << ", col = " << index.column(); + Wt::WString path = Wt::asString(index.data(CoverRole)); if (!path.empty()) { + LMS_LOG(MOD_UI, SEV_DEBUG) << "Path = " << path ; + // Create an image for this track Wt::WImage *image = new Wt::WImage( ); CoverResource *resource = new CoverResource(boost::filesystem::path(path.toUTF8()), 64, image); @@ -331,7 +349,7 @@ PlayQueue::addTracks(const std::vector& trackIds) _model->insertRows(dataRow, 1); - _model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id()); + _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->getPath(), CoverRole); _model->setData(dataRow, COLUMN_ID_NAME, track->getArtistName() + " - " + track->getName()); @@ -400,7 +418,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, COLUMN_ID_TRACK_ID)); + 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) From 5dc7818c124093147b34361a5528d5b4e8e9ea38 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 9 Dec 2014 12:44:57 +0100 Subject: [PATCH 04/11] [UI] Removed margins from the bottom of the nav bar --- src/ui/docroot/css/lms.css | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 48f18b91..791cd461 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -1,5 +1,8 @@ - -.playqueue-playing { - font-weight: bold; +.main-nav { + margin-bottom: 0px; +} + +.playqueue-playing { + font-weight: bold; } From c5937ead52cf09ef4fb48d426cd5ee58af890ed8 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 9 Dec 2014 13:00:10 +0100 Subject: [PATCH 05/11] [UI] Added left padding in the setings menu --- src/ui/docroot/css/lms.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 791cd461..605eaac6 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -2,6 +2,10 @@ margin-bottom: 0px; } +div.contents { + padding: 0px 12px 6px; +} + .playqueue-playing { font-weight: bold; } From a03a890f5bc9b4c08ef9603d1bcadacc1a0a936b Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 13 Dec 2014 18:21:57 +0100 Subject: [PATCH 06/11] [UI] Various improvments in audio view --- src/av/InputFormatContext.cpp | 13 +++++++++ src/av/InputFormatContext.hpp | 1 + src/database-updater/DatabaseUpdater.cpp | 5 ++++ src/database/AudioTypes.hpp | 4 +++ src/database/Track.cpp | 3 ++- src/metadata/AvFormat.cpp | 3 +++ src/metadata/MetaData.hpp | 8 +----- src/ui/audio/AudioMediaPlayer.cpp | 11 +++++--- src/ui/audio/PlayQueue.cpp | 33 +++++++++++++---------- src/ui/audio/TableFilter.cpp | 13 +++++---- src/ui/docroot/css/lms.css | 13 +++++++++ src/ui/docroot/images/unknown-cover.jpg | Bin 0 -> 1120 bytes src/ui/settings/Settings.cpp | 1 + 13 files changed, 78 insertions(+), 30 deletions(-) create mode 100755 src/ui/docroot/images/unknown-cover.jpg 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/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/audio/AudioMediaPlayer.cpp b/src/ui/audio/AudioMediaPlayer.cpp index c3308062..7efba7f8 100644 --- a/src/ui/audio/AudioMediaPlayer.cpp +++ b/src/ui/audio/AudioMediaPlayer.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "AudioMediaPlayer.hpp" @@ -30,6 +31,9 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent), _mediaResource(nullptr) { + + this->setStyleClass("mediaplayer"); + _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 ); @@ -52,14 +56,15 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WContainerWidget *parent) _shuffle.emit( shuffle->checkState() == Wt::Checked); })); + _curTime = new Wt::WText("00:00:00", container); + _timeSlider = new Wt::WSlider( container ); + _duration = new Wt::WText("00:00:00", container); + 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 ); - _curTime = new Wt::WText(container); - _timeSlider = new Wt::WSlider( container ); - _duration = new Wt::WText(container); _volumeSlider = new Wt::WSlider( container ); _volumeSlider->setRange(0,100); diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 3e13f5cc..8ea21ac7 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -20,10 +20,12 @@ #include #include +#include #include #include #include #include +#include #include "resource/CoverResource.hpp" @@ -82,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 @@ -125,7 +127,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))); @@ -157,7 +159,7 @@ TrackSelector::setShuffle(bool enable) } int -TrackSelector::getNext() +TrackSelector::next() { if (_size == 0) return trackPosInvalid; @@ -176,7 +178,7 @@ TrackSelector::getNext() } int -TrackSelector::getPrevious() +TrackSelector::previous() { if (_size == 0) return trackPosInvalid; @@ -229,16 +231,17 @@ class PlayQueueItemDelegate : public Wt::WItemDelegate { Wt::WWidget* res; - LMS_LOG(MOD_UI, SEV_DEBUG) << "Update called! widget = " << widget << ", row = " << index.row() << ", col = " << index.column(); - Wt::WString path = Wt::asString(index.data(CoverRole)); if (!path.empty()) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "Path = " << path ; - // Create an image for this track - Wt::WImage *image = new Wt::WImage( ); - CoverResource *resource = new CoverResource(boost::filesystem::path(path.toUTF8()), 64, image); + 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)); res = image; @@ -351,7 +354,7 @@ PlayQueue::addTracks(const std::vector& trackIds) _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->getPath(), CoverRole); + _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()); } @@ -383,7 +386,7 @@ PlayQueue::playNext(void) while (nbTries > 0) { - int pos = _trackSelector->getNext(); + int pos = _trackSelector->next(); if (pos == trackPosInvalid) break; @@ -401,7 +404,7 @@ PlayQueue::playPrevious(void) while (nbTries > 0) { - int pos = _trackSelector->getPrevious(); + int pos = _trackSelector->previous(); if (pos == trackPosInvalid) break; @@ -427,6 +430,8 @@ PlayQueue::readTrack(int rowPos) _sigTrackPlay.emit(track->getPath()); + this->scrollTo( _model->index(_trackSelector->getCurrent(), 0)); + return true; } else diff --git a/src/ui/audio/TableFilter.cpp b/src/ui/audio/TableFilter.cpp index 4c5fbf6d..5a521918 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 - 7 - trackColumnSize - 23 /*Asc wdth */); + 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 605eaac6..131ffa8d 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -2,6 +2,14 @@ margin-bottom: 0px; } +.Wt-hrh2 { + background-color: white; +} + +.Wt-vrh2 { + background-color: white; +} + div.contents { padding: 0px 12px 6px; } @@ -10,3 +18,8 @@ div.contents { font-weight: bold; } +.mediaplayer { + background-color: #ccc; + border-radius: 15px; +} + diff --git a/src/ui/docroot/images/unknown-cover.jpg b/src/ui/docroot/images/unknown-cover.jpg new file mode 100755 index 0000000000000000000000000000000000000000..19ffc70603848bfc9ccb2ba7a57b3e53f3c24ffe GIT binary patch literal 1120 zcmex=J{_I=b4Ls^MSn`pH6GNTmDdP z?|I+tHyG9>&Q93ivSd^73D!R*GH18Gy^(+Iim>?6^%8p|^DUHrPyQtMCD?IM!VCA< zf2F6p*{58(!4sjUJ1MEAW{Vs9kLSi*4Eca;sdE zFGuEGJE!wnq+n+Ci6xxUC#EYGUypsea{E%vsqWFWC-;g!wydo8*$=fBrzRD%0FJN3zx)WtQ>o8Fo^L$tv#wUS8|7S9>B@|de5|Iq_9NeN$0SGIyy;6`7|lrD zdhPkOnH3fl#g7#Nm{@y{?|jbwL7Q7Owp8qNz@;ER)40$>=X{%{`_J6?Pi^XxqpNyX zzVFzeZg48?@X|fe#cm#d59epUt&ch*8zH2!YeJ5r*jB%f)nR87TCC0QT{%7z_ zRNb@WZB^#Oc`Ap}Boa-6l)Rd!Wz5{_eB;-@+`^?w{+f0!>udJwANK8zyr=EO+S28< zf&EH$cn_D#^ofPKKC}E%XFBu5*}P%6DUucOP4=wLk6vZt%>rk<1#e8QTBoBT;1?M% z{V9XBVcPbc*A}nWytK(@(>rS^`-2_FVp5J|xMbMH9Qo&}*SZO+XWIy(ig z=&Y;p)Uhvo`8t7N|56j4_e=hnK0H|{^Rq2#`t-^~>HWX;*B4&DU(Pjk$rr9!Kax6n zB-&UE%oKVZ7yXQiXpR!go%nR;ME#Bi&fG+;%IMeEpI3?OQBlr5+^^{F-aX^5qkPxm z6SFSLyT9MYKHoj~PDo8%bV~@&fy{U+q6X4ZN7! jxT9Vr(de;Z2Sc<;;D#%*hvfq=l-#eKnH923`TtD-N!6}S literal 0 HcmV?d00001 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); From d1c4678d82f2c3b7c233f580bee9c5a8c4c42d49 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Dec 2014 17:06:12 +0100 Subject: [PATCH 07/11] [UI] corrected segfault when enabling shuffle on en empty play queue --- src/ui/audio/PlayQueue.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 8ea21ac7..3e17d9ef 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -111,6 +111,7 @@ class TrackSelector TrackSelector::TrackSelector() : _loop(false), _shuffle(false), +_size(0), _curPos(0) { } @@ -152,10 +153,14 @@ 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 From 799d7a4b32db68242edf13091e968c257a7900a0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Dec 2014 17:09:29 +0100 Subject: [PATCH 08/11] [UI] Slightly better looking MediaPlayer --- src/ui/approot/templates.xml | 10 +++ src/ui/audio/Audio.cpp | 10 +-- src/ui/audio/AudioMediaPlayer.cpp | 134 ++++++++++++++++++------------ src/ui/audio/TableFilter.cpp | 2 +- src/ui/docroot/css/lms.css | 2 +- 5 files changed, 97 insertions(+), 61 deletions(-) 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 abcaa61d..766f880b 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -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,11 +145,6 @@ _playQueue(nullptr) playQueueLayout->addLayout(playlistControls); - playQueueLayout->addWidget( _playQueue, 1); - - _mediaPlayer = new AudioMediaPlayer(); - playQueueLayout->addWidget(_mediaPlayer); - mainLayout->addLayout(playQueueLayout, 0, 0, 2, 1); } diff --git a/src/ui/audio/AudioMediaPlayer.cpp b/src/ui/audio/AudioMediaPlayer.cpp index 7efba7f8..610c59f2 100644 --- a/src/ui/audio/AudioMediaPlayer.cpp +++ b/src/ui/audio/AudioMediaPlayer.cpp @@ -17,11 +17,12 @@ * along with LMS. If not, see . */ - #include #include #include #include +#include +#include #include "AudioMediaPlayer.hpp" @@ -31,69 +32,94 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent), _mediaResource(nullptr) { - 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->setAlternativeContent (new Wt::WText("You don't have HTML5 audio support!")); -// _mediaPlayer->setOptions( Wt::WMediaPlayer::Autoplay ); _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); - _curTime = new Wt::WText("00:00:00", container); - _timeSlider = new Wt::WSlider( container ); - _duration = new Wt::WText("00:00:00", container); + nextBtn->clicked().connect(std::bind([=] () + { + _mediaPlayer->stop(); + _playNext.emit(); + })); - 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 ); - - - _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/TableFilter.cpp b/src/ui/audio/TableFilter.cpp index 5a521918..b2c156db 100644 --- a/src/ui/audio/TableFilter.cpp +++ b/src/ui/audio/TableFilter.cpp @@ -74,7 +74,7 @@ TableFilter::layoutSizeChanged (int width, int height) std::size_t trackColumnSize = this->columnWidth(1).toPixels(); // Set the remaining size for the name column - this->setColumnWidth(0, width - 7 - trackColumnSize - 23 /*Asc wdth */); + this->setColumnWidth(0, width - trackColumnSize - (7 * 2) - 2); LMS_LOG(MOD_UI, SEV_DEBUG) << "After: sizes = " << this->columnWidth(0).toPixels() << ", " << this->columnWidth(1).toPixels(); } diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css index 131ffa8d..1b5ed3f8 100644 --- a/src/ui/docroot/css/lms.css +++ b/src/ui/docroot/css/lms.css @@ -20,6 +20,6 @@ div.contents { .mediaplayer { background-color: #ccc; - border-radius: 15px; + border-radius: 10px; } From 4517e5d2acef9a2ad2671fd78dfd9fb242a97dae Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Dec 2014 17:50:35 +0100 Subject: [PATCH 09/11] [UI] minor source reorg --- src/ui/LmsApplication.cpp | 38 ++++++++++++++++++++++++-------------- src/ui/LmsApplication.hpp | 2 ++ 2 files changed, 26 insertions(+), 14 deletions(-) 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; From 730ad3b201b4f5adc92e5520033907ac3f4a56cc Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Dec 2014 22:52:42 +0100 Subject: [PATCH 10/11] [TEST] Added some verbose stats --- test/RemoteClient.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/RemoteClient.cpp b/test/RemoteClient.cpp index b95c7749..c37a9feb 100644 --- a/test/RemoteClient.cpp +++ b/test/RemoteClient.cpp @@ -810,6 +810,7 @@ int main() std::vector tracks; client.getTracks(tracks); + std::cout << "Got " << tracks.size() << " tracks!" << std::endl; BOOST_FOREACH(const TrackInfo& track, tracks) std::cout << "Track: '" << track << "'" << std::endl; @@ -820,10 +821,11 @@ int main() // Get the artists for each genre BOOST_FOREACH(const GenreInfo& genre, genres) { - std::cout << "Getting artists from genre '" << genre.name << "'" << std::endl; + std::cout << "Getting artists from genre '" << genre.name << "'... "; std::vector artists; client.getArtists(artists, std::vector(1, genre.name)); + std::cout << "Found " << artists.size() << " artists!" << std::endl; BOOST_FOREACH(const ArtistInfo& artist, artists) std::cout << "Genre '" << genre.name << "' -> Artist: " << artist << std::endl; } @@ -834,10 +836,11 @@ int main() std::cout << "Getting release for each artist..." << std::endl; BOOST_FOREACH(const ArtistInfo& artist, artists) { - std::cout << "Getting release from artist '" << artist.name << "'" << std::endl; + std::cout << "Getting release from artist '" << artist.name << "'... "; std::vector releases; client.getReleases(releases, std::vector(1, artist.name) ); + std::cout << "Found " << releases.size() << " releases!" << std::endl; BOOST_FOREACH(const ReleaseInfo& release, releases) std::cout << "Artist '" << artist.name << "' -> Release: '" << release << "'" << std::endl; } @@ -848,10 +851,11 @@ int main() std::cout << "Getting release for each genre..." << std::endl; BOOST_FOREACH(const GenreInfo& genre, genres) { - std::cout << "Getting release from genre '" << genre.name << "'" << std::endl; + std::cout << "Getting release from genre '" << genre.name << "'... "; std::vector releases; client.getReleases(releases, std::vector(), std::vector(1, genre.name)); + std::cout << "Found " << releases.size() << " releases!" << std::endl; BOOST_FOREACH(const ReleaseInfo& release, releases) std::cout << "Genre '" << genre.name << "' -> Release: '" << release << "'" << std::endl; } From 30c5858fa24504c44a2d54f4b1f35e6168ef1c4e Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Dec 2014 22:56:03 +0100 Subject: [PATCH 11/11] [DB] Corrected searching by genre and added searching by multi keywords --- TODO | 1 - src/Makefile.am | 2 - src/cover/CoverArtGrabber.cpp | 2 +- src/database/AudioTypes.hpp | 54 ++- src/database/DatabaseHandler.cpp | 1 + src/database/Genre.cpp | 68 --- src/database/SqlQuery.cpp | 2 +- src/database/Track.cpp | 388 +++++++++++++----- .../server/AudioCollectionRequestHandler.cpp | 44 +- src/ui/audio/Audio.cpp | 8 +- src/ui/audio/Filter.hpp | 11 +- src/ui/audio/FilterChain.cpp | 6 +- src/ui/audio/KeywordSearchFilter.cpp | 25 +- src/ui/audio/KeywordSearchFilter.hpp | 4 +- src/ui/audio/TableFilter.cpp | 82 ++-- src/ui/audio/TableFilter.hpp | 11 +- src/ui/audio/TableFilterGenre.cpp | 133 ------ src/ui/audio/TableFilterGenre.hpp | 65 --- src/ui/audio/TrackView.cpp | 63 +-- src/ui/audio/TrackView.hpp | 6 +- 20 files changed, 446 insertions(+), 530 deletions(-) delete mode 100644 src/database/Genre.cpp delete mode 100644 src/ui/audio/TableFilterGenre.cpp delete mode 100644 src/ui/audio/TableFilterGenre.hpp diff --git a/TODO b/TODO index 45d48e95..e956e043 100644 --- a/TODO +++ b/TODO @@ -39,7 +39,6 @@ [Audio] - Style eveything nicely... - - Search patterns in the genre list - ReleaseView/ArtistView/GenreView: -> Track count for this special category. Easier: add a special entry '' that contains everything? - MediaPlayer: move slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478) - TrackView : handle duration > 1 hour diff --git a/src/Makefile.am b/src/Makefile.am index 6912e71a..b8480855 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -12,7 +12,6 @@ lms_SOURCES = \ $(srcdir)/cover/CoverArt.cpp \ $(srcdir)/cover/CoverArtGrabber.cpp \ $(srcdir)/database/DatabaseHandler.cpp \ - $(srcdir)/database/Genre.cpp \ $(srcdir)/database/MediaDirectory.cpp \ $(srcdir)/database/SqlQuery.cpp \ $(srcdir)/database/Track.cpp \ @@ -46,7 +45,6 @@ lms_SOURCES = \ $(srcdir)/ui/audio/KeywordSearchFilter.cpp \ $(srcdir)/ui/audio/PlayQueue.cpp \ $(srcdir)/ui/audio/TableFilter.cpp \ - $(srcdir)/ui/audio/TableFilterGenre.cpp \ $(srcdir)/ui/audio/TrackView.cpp \ $(srcdir)/ui/common/DirectoryValidator.cpp \ $(srcdir)/ui/common/SessionData.cpp \ diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index dccfaf77..8ca350ff 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -79,7 +79,7 @@ Grabber::getFromTrack(Database::Track::pointer track) { std::vector res; - if (!track) + if (!track || !track->hasCover()) return std::vector(); try diff --git a/src/database/AudioTypes.hpp b/src/database/AudioTypes.hpp index 08ccc1d5..eeacb22c 100644 --- a/src/database/AudioTypes.hpp +++ b/src/database/AudioTypes.hpp @@ -32,6 +32,25 @@ namespace Database { +// Find utilities +struct SearchFilter +{ + enum class Field { + Artist, // artist name + Release, // release name + Genre, // genre name + Track, // track name + }; + + typedef std::map > FieldValues; + + // Formulas : + // ((like1-1 LIKE ? OR like1-2 LIKE = ? ...) AND (like2-1 LIKE ? OR like2-2 LIKE ? ...)) AND exact1 = ? AND exact2 = ? ... + + std::vector likeMatches; + FieldValues exactMatch; +}; + class Track; class Genre @@ -47,7 +66,9 @@ class Genre // Find utility static pointer getByName(Wt::Dbo::Session& session, const std::string& name); static pointer getNone(Wt::Dbo::Session& session); - static Wt::Dbo::collection getAll(Wt::Dbo::Session& session, std::size_t offset = -1, std::size_t size = -1); + static Wt::Dbo::collection getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1); + static Wt::Dbo::Query > getAllQuery(Wt::Dbo::Session& session, SearchFilter& filter); + static void updateGenreQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel >& model, SearchFilter filter, const std::vector& columnNames = std::vector()); // Create utility static pointer create(Wt::Dbo::Session& session, const std::string& name); @@ -81,25 +102,21 @@ class Track Track() {} Track(const boost::filesystem::path& p); - // Find utilities + + // Find utility functions static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p); static pointer getById(Wt::Dbo::Session& session, id_type id); static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session); - static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session, - const std::vector& artists, // OR filter if many - const std::vector& releases, // OR filter if many - const std::vector& genres, // OR filter if many - int offset = -1, int size = -1); - - static std::vector getReleases(Wt::Dbo::Session& session, - const std::vector& artists, // OR filter if many - const std::vector& genres, // OR filter if many - int offset = -1, int size = -1); - - static std::vector getArtists(Wt::Dbo::Session& session, - const std::vector& genres, // OR filter - int offset = -1, int size = -1); + // Used for remote + static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); + static std::vector getReleases(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); + static std::vector getArtists(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); + // Utility fonctions + // MVC models for the user interface + static void updateTracksQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< pointer >& model, SearchFilter filter, const std::vector& columnNames = std::vector()); + static void updateReleaseQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel >& model, SearchFilter filter, const std::vector& columnNames = std::vector()); + static void updateArtistQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel >& model, SearchFilter filter, const std::vector& columnNames = std::vector()); // Create utility static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p); @@ -156,6 +173,10 @@ class Track private: + static Wt::Dbo::Query< pointer > getAllQuery(Wt::Dbo::Session& session, SearchFilter filter); + static Wt::Dbo::Query > getReleasesQuery(Wt::Dbo::Session& session, SearchFilter filter); + static Wt::Dbo::Query > getArtistsQuery(Wt::Dbo::Session& session, SearchFilter filter); + static const std::size_t _maxNameLength = 128; int _trackNumber; @@ -175,6 +196,7 @@ class Track Wt::Dbo::collection< Genre::pointer > _genres; // Tracks that belong to this genre }; + } // namespace database #endif diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index c0f7ddeb..23dd7431 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -98,6 +98,7 @@ _dbBackend( db.string() ) _session.createTables(); _dbBackend.executeSql("CREATE INDEX artist_name_idx ON track(artist_name)"); _dbBackend.executeSql("CREATE INDEX release_name_idx ON track(release_name)"); + _dbBackend.executeSql("CREATE INDEX genre_name_idx ON genre(name)"); } catch(std::exception& e) { LMS_LOG(MOD_DB, SEV_ERROR) << "Cannot create tables: " << e.what(); diff --git a/src/database/Genre.cpp b/src/database/Genre.cpp deleted file mode 100644 index c0d1f5e7..00000000 --- a/src/database/Genre.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2013 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 "AudioTypes.hpp" - -namespace Database { - -Genre::Genre() -{ -} - -Genre::Genre(const std::string& name) -: _name( std::string(name, 0, _maxNameLength) ) -{ -} - - -Genre::pointer -Genre::getByName(Wt::Dbo::Session& session, const std::string& name) -{ - // TODO use like search - return session.find().where("name = ?").bind( std::string(name, 0, _maxNameLength) ); -} - -Genre::pointer -Genre::getNone(Wt::Dbo::Session& session) -{ - pointer res = getByName(session, ""); - if (!res) - res = create(session, ""); - return res; -} - -bool -Genre::isNone(void) const -{ - return (_name == ""); -} - -Genre::pointer -Genre::create(Wt::Dbo::Session& session, const std::string& name) -{ - return session.add(new Genre(name)); -} - -Wt::Dbo::collection -Genre::getAll(Wt::Dbo::Session& session, std::size_t offset, std::size_t size) -{ - return session.find().offset(offset).limit(size); -} - -} // namespace Database diff --git a/src/database/SqlQuery.cpp b/src/database/SqlQuery.cpp index 5dc6a12c..611e1c9e 100644 --- a/src/database/SqlQuery.cpp +++ b/src/database/SqlQuery.cpp @@ -89,7 +89,7 @@ InnerJoinClause::And(const InnerJoinClause& clause) if (!_clause.empty()) _clause += " "; - _clause += clause._clause; + _clause += "INNER JOIN " + clause._clause; return *this; } diff --git a/src/database/Track.cpp b/src/database/Track.cpp index f2df60e2..3312d5c4 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -19,12 +19,114 @@ #include +#include + +#include "logger/Logger.hpp" + #include "SqlQuery.hpp" #include "AudioTypes.hpp" namespace Database { +static SqlQuery generatePartialQuery(SearchFilter& filter, bool forceGenreInnerJoin = false) +{ + bool genreJoin = forceGenreInnerJoin; + SqlQuery sqlQuery; + + + // Process like searches + BOOST_FOREACH(SearchFilter::FieldValues& likeMatch, filter.likeMatches) + { + WhereClause likeWhereClause; + + // Artist + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, likeMatch[SearchFilter::Field::Artist]) + whereClause.Or( WhereClause("t.artist_name LIKE ?") ).bind("%%" + name + "%%"); + + likeWhereClause.Or( whereClause ); + } + // Release + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, likeMatch[SearchFilter::Field::Release]) + whereClause.Or( WhereClause("t.release_name LIKE ?") ).bind("%%" + name + "%%"); + + likeWhereClause.Or( whereClause ); + } + // Genre + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, likeMatch[SearchFilter::Field::Genre]) + { + whereClause.Or( WhereClause("g.name LIKE ?") ).bind("%%" + name + "%%"); + genreJoin = true; + } + + likeWhereClause.Or( whereClause ); + } + // Track + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, likeMatch[SearchFilter::Field::Track]) + whereClause.Or( WhereClause("t.name LIKE ?") ).bind("%%" + name + "%%"); + + likeWhereClause.Or( whereClause ); + } + + sqlQuery.where().And( likeWhereClause ); + } + + // Add exact search constraints + // Artist + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, filter.exactMatch[SearchFilter::Field::Artist]) + whereClause.Or( WhereClause("t.artist_name = ?") ).bind(name); + + sqlQuery.where().And( whereClause ); + } + + // Release + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, filter.exactMatch[SearchFilter::Field::Release]) + whereClause.Or( WhereClause("t.release_name = ?") ).bind(name); + + sqlQuery.where().And( whereClause ); + } + + // Genre + { + WhereClause whereClause; + + BOOST_FOREACH(const std::string& name, filter.exactMatch[SearchFilter::Field::Genre]) + { + whereClause.Or( WhereClause("g.name = ?") ).bind(name); + genreJoin = true; + } + + sqlQuery.where().And( whereClause ); + } + + if (genreJoin) + { + sqlQuery.innerJoin().And( InnerJoinClause("genre g ON g.id = t_g.genre_id")); + sqlQuery.innerJoin().And( InnerJoinClause("track_genre t_g ON t_g.track_id = t.id")); + } + + return sqlQuery; +} + + Track::Track(const boost::filesystem::path& p) : _trackNumber(0), @@ -34,6 +136,11 @@ _hasCover(false) { } +Wt::Dbo::collection< Track::pointer > +Track::getAll(Wt::Dbo::Session& session) +{ + return session.find(); +} void Track::setGenres(std::vector genres) @@ -65,12 +172,6 @@ Track::create(Wt::Dbo::Session& session, const boost::filesystem::path& p) return session.add(new Track(p) ); } -Wt::Dbo::collection< Track::pointer > -Track::getAll(Wt::Dbo::Session& session) -{ - return session.find(); -} - std::vector< Genre::pointer > Track::getGenres(void) const { @@ -79,89 +180,116 @@ Track::getGenres(void) const return genres; } -Wt::Dbo::collection< Track::pointer > -Track::getAll(Wt::Dbo::Session& session, - const std::vector& artists, - const std::vector& releases, - const std::vector& genres, - int offset, int size) +Wt::Dbo::Query< Track::pointer > +Track::getAllQuery(Wt::Dbo::Session& session, SearchFilter filter) { - std::string sqlQuery = "SELECT t FROM track t"; + SqlQuery sqlQuery = generatePartialQuery(filter); - if (!genres.empty()) - { - sqlQuery += " INNER JOIN genre g ON g.id = t_g.genre_id"; - sqlQuery += " INNER JOIN track_genre t_g ON t_g.track_id = t.id AND t_g.genre_id = g.id"; - } + Wt::Dbo::Query query + = session.query( "SELECT t FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("t.id"); - WhereClause where; - - { - WhereClause artistWhere; - - for (std::size_t i = 0; i < artists.size(); ++i) - artistWhere.Or( WhereClause("t.artist_name = ?") ); - - where.And(artistWhere); - } - - { - WhereClause releaseWhere; - - for (std::size_t i = 0; i < releases.size(); ++i) - releaseWhere.Or( WhereClause("t.release_name = ?") ); - - where.And(releaseWhere); - } - - { - WhereClause genreWhere; - - for (std::size_t i = 0; i < genres.size(); ++i) - genreWhere.Or( WhereClause("g.name = ?") ); - - where.And(genreWhere); - } - - - Wt::Dbo::Query query = session.query( sqlQuery + " " + where.get() ).offset(offset).limit(size); - - BOOST_FOREACH(const std::string& artist, artists) - query.bind(artist); - - BOOST_FOREACH(const std::string& release, releases) - query.bind(release); - - BOOST_FOREACH(const std::string& genre, genres) - query.bind(genre); - - query.groupBy("t"); + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) + query.bind(bindArg); return query; } -std::vector -Track::getArtists(Wt::Dbo::Session& session, - const std::vector& genres, - int offset, int size) +Wt::Dbo::collection< Track::pointer > +Track::getAll(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) { - std::string sqlQuery = "SELECT t.artist_name FROM track t"; + return getAllQuery(session, filter).limit(size).offset(offset); +} - if (!genres.empty()) +Wt::Dbo::Query< boost::tuple > +Track::getReleasesQuery(Wt::Dbo::Session& session, SearchFilter filter) +{ + SqlQuery sqlQuery = generatePartialQuery(filter); + + Wt::Dbo::Query > query + = session.query >("SELECT t.release_name, COUNT(DISTINCT t.id) FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("t.release_name").orderBy("t.release_name"); + + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) + query.bind(bindArg); + + return query; +} + +Wt::Dbo::Query< boost::tuple > +Track::getArtistsQuery(Wt::Dbo::Session& session, SearchFilter filter) +{ + SqlQuery sqlQuery = generatePartialQuery(filter); + + Wt::Dbo::Query > query + = session.query >( "SELECT t.artist_name, COUNT(DISTINCT t.id) FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("t.artist_name").orderBy("t.artist_name"); + + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) + query.bind(bindArg); + + return query; +} + +void +Track::updateTracksQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< pointer >& model, SearchFilter filter, const std::vector& columnNames) +{ + Wt::Dbo::Query< pointer > query = getAllQuery(session, filter).orderBy("t.artist_name,t.date,t.release_name,t.disc_number,t.track_number"); + model.setQuery(query, columnNames.empty() ? true : false); + + // TODO do something better + if (columnNames.size() == 9) { - sqlQuery += " INNER JOIN genre g ON g.id = t_g.genre_id"; - sqlQuery += " INNER JOIN track_genre t_g ON t_g.track_id = t.id AND t_g.genre_id = g.id"; + model.addColumn( "t.artist_name", columnNames[0] ); + model.addColumn( "t.release_name", columnNames[1] ); + model.addColumn( "t.disc_number", columnNames[2] ); + model.addColumn( "t.track_number", columnNames[3] ); + model.addColumn( "t.name", columnNames[4] ); + model.addColumn( "t.duration", columnNames[5] ); + model.addColumn( "t.date", columnNames[6] ); + model.addColumn( "t.original_date", columnNames[7] ); + model.addColumn( "t.genre_list", columnNames[8] ); } - WhereClause whereClause; +} - for (std::size_t i = 0; i < genres.size(); ++i) - whereClause.Or( WhereClause("g.name = ?") ); +void +Track::updateReleaseQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel >& model, SearchFilter filter, const std::vector& columnNames) +{ + Wt::Dbo::Query< boost::tuple > query = getReleasesQuery(session, filter); - Wt::Dbo::Query query = session.query( sqlQuery + " " + whereClause.get() ).offset(offset).limit(size).groupBy("t.artist_name"); + model.setQuery(query, columnNames.empty() ? true : false); - BOOST_FOREACH(const std::string& genre, genres) - query.bind(genre); + // TODO do something better + if (columnNames.size() == 2) + { + model.addColumn( "t.release_name", columnNames[0]); + model.addColumn( "COUNT(DISTINCT t.id)" ); + } +} + +void +Track::updateArtistQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel >& model, SearchFilter filter, const std::vector& columnNames) +{ + Wt::Dbo::Query< boost::tuple > query = getArtistsQuery(session, filter); + + model.setQuery(query, columnNames.empty() ? true : false); + + // TODO do something better + if (columnNames.size() == 2) + { + model.addColumn( "t.artist_name", columnNames[0]); + model.addColumn( "COUNT(DISTINCT t.id)" ); + } +} + +std::vector +Track::getArtists(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) +{ + SqlQuery sqlQuery = generatePartialQuery(filter); + + Wt::Dbo::Query query + = session.query( "SELECT DISTINCT t.artist_name FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).offset(offset).limit(size).orderBy("t.artist_name"); + + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) + query.bind(bindArg); typedef Wt::Dbo::collection< std::string > ArtistNames; ArtistNames artistNames(query); @@ -171,46 +299,17 @@ Track::getArtists(Wt::Dbo::Session& session, res.push_back((*it)); return res; - } std::vector -Track::getReleases(Wt::Dbo::Session& session, - const std::vector& artists, - const std::vector& genres, - int offset, int size) +Track::getReleases(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) { + SqlQuery sqlQuery = generatePartialQuery(filter); - std::string sqlQuery = "SELECT t.release_name FROM track t"; + Wt::Dbo::Query query + = session.query( "SELECT DISTINCT t.release_name FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).offset(offset).limit(size).orderBy("t.release_name"); - if (!genres.empty()) - { - sqlQuery += " INNER JOIN genre g ON g.id = t_g.genre_id"; - sqlQuery += " INNER JOIN track_genre t_g ON t_g.track_id = t.id AND t_g.genre_id = g.id"; - } - - WhereClause whereClause; - - { - WhereClause clause; - - BOOST_FOREACH(const std::string& artist, artists) - clause.Or( WhereClause("t.artist_name = ?") ).bind(artist); - - whereClause.And(clause); - } - { - WhereClause clause; - - BOOST_FOREACH(const std::string& genre, genres) - clause.Or( WhereClause("g.name = ?") ).bind(genre); - - whereClause.And(clause); - } - - Wt::Dbo::Query query = session.query( sqlQuery + " " + whereClause.get() ).offset(offset).limit(size).groupBy("t.release_name"); - - BOOST_FOREACH(const std::string& bindArg, whereClause.getBindArgs()) + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) query.bind(bindArg); typedef Wt::Dbo::collection< std::string > ReleaseNames; @@ -223,8 +322,77 @@ Track::getReleases(Wt::Dbo::Session& session, return res; } +Genre::Genre() +{ +} + +Genre::Genre(const std::string& name) +: _name( std::string(name, 0, _maxNameLength) ) +{ +} + + +Genre::pointer +Genre::getByName(Wt::Dbo::Session& session, const std::string& name) +{ + // TODO use like search + return session.find().where("name = ?").bind( std::string(name, 0, _maxNameLength) ); +} + +Genre::pointer +Genre::getNone(Wt::Dbo::Session& session) +{ + pointer res = getByName(session, ""); + if (!res) + res = create(session, ""); + return res; +} + +bool +Genre::isNone(void) const +{ + return (_name == ""); +} + +Genre::pointer +Genre::create(Wt::Dbo::Session& session, const std::string& name) +{ + return session.add(new Genre(name)); +} + +Wt::Dbo::collection +Genre::getAll(Wt::Dbo::Session& session, int offset, int size) +{ + return session.find().offset(offset).limit(size).orderBy("name"); +} + +Wt::Dbo::Query > +Genre::getAllQuery(Wt::Dbo::Session& session, SearchFilter& filter) +{ + SqlQuery sqlQuery = generatePartialQuery(filter, true); + + Wt::Dbo::Query > query + = session.query >( "SELECT g.name, COUNT(DISTINCT t.id) FROM track t " + sqlQuery.innerJoin().get() + " " + sqlQuery.where().get()).groupBy("g.name").orderBy("g.name"); + + BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) + query.bind(bindArg); + + return query; +} + +void +Genre::updateGenreQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< boost::tuple >& model, SearchFilter filter, const std::vector& columnNames) +{ + Wt::Dbo::Query< boost::tuple > query = getAllQuery(session, filter); + model.setQuery(query, columnNames.empty() ? true : false); + + // TODO do something better + if (columnNames.size() == 2) + { + model.addColumn( "g.name", columnNames[0] ); + model.addColumn( "COUNT(DISTINCT t.id)", columnNames[1] ); + } +} } // namespace Database - - diff --git a/src/remote/server/AudioCollectionRequestHandler.cpp b/src/remote/server/AudioCollectionRequestHandler.cpp index 17719257..98d9e37f 100644 --- a/src/remote/server/AudioCollectionRequestHandler.cpp +++ b/src/remote/server/AudioCollectionRequestHandler.cpp @@ -34,6 +34,8 @@ namespace Remote { namespace Server { +using namespace Database; + AudioCollectionRequestHandler::AudioCollectionRequestHandler(Database::Handler& db) : _db(db) {} @@ -163,24 +165,23 @@ AudioCollectionRequestHandler::processGetArtists(const AudioCollectionRequest::G size = _maxListArtists; size = std::min(size, _maxListArtists); + SearchFilter filter; + // Get filters std::vector genres; for (int id = 0; id < request.genre_size(); ++id) - genres.push_back( request.genre(id) ); + filter.exactMatch[SearchFilter::Field::Genre].push_back( request.genre(id) ); // Now fetch requested data... Wt::Dbo::Transaction transaction( _db.getSession() ); std::vector artists - = Database::Track::getArtists(_db.getSession(), - genres, + = Database::Track::getArtists(_db.getSession(), filter, request.batch_parameter().offset(), static_cast(size) ); BOOST_FOREACH(const std::string& artist, artists) - { response.add_artists()->set_name( std::string( boost::locale::conv::to_utf(artist, "UTF-8") ) ); - } return true; } @@ -200,20 +201,20 @@ AudioCollectionRequestHandler::processGetReleases(const AudioCollectionRequest:: size = _maxListReleases; size = std::min(size, _maxListReleases); + SearchFilter filter; + std::vector artists; for (int id = 0; id < request.artist_size(); ++id) - artists.push_back( request.artist(id) ); + filter.exactMatch[SearchFilter::Field::Artist].push_back( request.artist(id) ); std::vector genres; for (int id = 0; id < request.genre_size(); ++id) - genres.push_back( request.genre(id) ); + filter.exactMatch[SearchFilter::Field::Genre].push_back( request.genre(id) ); Wt::Dbo::Transaction transaction( _db.getSession() ); std::vector releases - = Database::Track::getReleases( _db.getSession(), - artists, - genres, + = Database::Track::getReleases( _db.getSession(), filter, request.batch_parameter().offset(), static_cast(size)); BOOST_FOREACH(const std::string& release, releases) @@ -240,25 +241,24 @@ AudioCollectionRequestHandler::processGetTracks(const AudioCollectionRequest::Ge size = std::min(size, _maxListTracks); // Get filters + SearchFilter filter; + std::vector artists; for (int id = 0; id < request.artist_size(); ++id) - artists.push_back( request.artist(id) ); + filter.exactMatch[SearchFilter::Field::Artist].push_back( request.artist(id) ); std::vector releases; for (int id = 0; id < request.release_size(); ++id) - releases.push_back( request.release(id) ); + filter.exactMatch[SearchFilter::Field::Release].push_back( request.release(id) ); std::vector genres; for (int id = 0; id < request.genre_size(); ++id) - genres.push_back( request.genre(id) ); + filter.exactMatch[SearchFilter::Field::Genre].push_back( request.genre(id) ); Wt::Dbo::Transaction transaction( _db.getSession() ); Wt::Dbo::collection tracks - = Database::Track::getAll( _db.getSession(), - artists, - releases, - genres, + = Database::Track::getAll( _db.getSession(), filter, request.batch_parameter().offset(), static_cast(size)); typedef Wt::Dbo::collection< Database::Track::pointer > Tracks; @@ -305,11 +305,11 @@ AudioCollectionRequestHandler::processGetCoverArt(const AudioCollectionRequest:: case AudioCollectionRequest::GetCoverArt::TypeGetCoverArtRelease: if (request.has_release()) { - Wt::Dbo::collection tracks = Database::Track::getAll(_db.getSession(), - std::vector(), // artist - std::vector(1, request.release()), // release - std::vector(), // genre - -1, 1); + SearchFilter filter; + filter.exactMatch[SearchFilter::Field::Release].push_back(request.release()); + + Wt::Dbo::collection tracks + = Database::Track::getAll(_db.getSession(), filter, -1, 1 /* limit reuslt size */); Wt::Dbo::collection::iterator it = tracks.begin(); diff --git a/src/ui/audio/Audio.cpp b/src/ui/audio/Audio.cpp index 766f880b..ec8f8412 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -29,7 +29,6 @@ #include "logger/Logger.hpp" #include "TableFilter.hpp" -#include "TableFilterGenre.hpp" #include "KeywordSearchFilter.hpp" #include "Audio.hpp" @@ -49,15 +48,15 @@ _playQueue(nullptr) // Filters Wt::WHBoxLayout *filterLayout = new Wt::WHBoxLayout(); - TableFilterGenre *filterGenre = new TableFilterGenre(_db); + TableFilter *filterGenre = new TableFilter(_db, Database::SearchFilter::Field::Genre, { "Genre", "Tracks"} ); filterLayout->addWidget(filterGenre); _filterChain.addFilter(filterGenre); - TableFilter *filterArtist = new TableFilter(_db, "track", "artist_name", "Artist"); + TableFilter *filterArtist = new TableFilter(_db, Database::SearchFilter::Field::Artist, {"Artist", "Tracks"} ); filterLayout->addWidget(filterArtist); _filterChain.addFilter(filterArtist); - TableFilter *filterRelease = new TableFilter(_db, "track", "release_name", "Release"); + TableFilter *filterRelease = new TableFilter(_db, Database::SearchFilter::Field::Release, {"Release", "Tracks"}); filterLayout->addWidget(filterRelease); _filterChain.addFilter(filterRelease); @@ -95,6 +94,7 @@ _playQueue(nullptr) Database::User::pointer user = _db.getCurrentUser(); Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); + playQueueLayout->setContentsMargins(5,5,5,5); _mediaPlayer = new AudioMediaPlayer(); playQueueLayout->addWidget(_mediaPlayer); diff --git a/src/ui/audio/Filter.hpp b/src/ui/audio/Filter.hpp index 162cca41..17afe7d2 100644 --- a/src/ui/audio/Filter.hpp +++ b/src/ui/audio/Filter.hpp @@ -22,7 +22,7 @@ #include -#include "database/SqlQuery.hpp" +#include "database/AudioTypes.hpp" namespace UserInterface { @@ -31,17 +31,20 @@ class Filter public: struct Constraint { - WhereClause where; // WHERE SQL clause + std::vector search; + + typedef std::map > ColumnValues; + ColumnValues columnValues; }; Filter() {} virtual ~Filter() {} // Refresh filter using constraints created by parent filters - virtual void refresh(const Constraint& constraint) = 0; + virtual void refresh(Database::SearchFilter& filter) = 0; // Update constraints for next filters - virtual void getConstraint(Constraint& constraint) = 0; + virtual void getConstraint(Database::SearchFilter& filter) = 0; // Emitted when a constraint has changed Wt::Signal& update() { return _update; }; diff --git a/src/ui/audio/FilterChain.cpp b/src/ui/audio/FilterChain.cpp index 403a6d2e..b3b0ab96 100644 --- a/src/ui/audio/FilterChain.cpp +++ b/src/ui/audio/FilterChain.cpp @@ -50,7 +50,7 @@ FilterChain::updateFilters(std::size_t startIdx) _refreshingFilters = true; - Filter::Constraint currentConstraint; + Database::SearchFilter searchFilter; for (std::size_t idFilter = 0; idFilter < _filters.size(); ++idFilter) { @@ -58,12 +58,12 @@ FilterChain::updateFilters(std::size_t startIdx) // Apply contraints created by previous filters if (idFilter > startIdx) { - filter->refresh(currentConstraint); + filter->refresh(searchFilter); } // Get constraints generated by this filter // (Note: adding accross successive calls) - filter->getConstraint(currentConstraint); + filter->getConstraint(searchFilter); } _refreshingFilters = false; diff --git a/src/ui/audio/KeywordSearchFilter.cpp b/src/ui/audio/KeywordSearchFilter.cpp index 9782a66a..8f896939 100644 --- a/src/ui/audio/KeywordSearchFilter.cpp +++ b/src/ui/audio/KeywordSearchFilter.cpp @@ -17,6 +17,10 @@ * along with LMS. If not, see . */ +#include +#include +#include + #include "KeywordSearchFilter.hpp" namespace UserInterface { @@ -34,15 +38,26 @@ KeywordSearchFilter::setText(const std::string& text) // Get constraints created by this filter void -KeywordSearchFilter::getConstraint(Constraint& constraint) +KeywordSearchFilter::getConstraint(Database::SearchFilter& filter) { // No active search means no constaint! if (!_lastEmittedText.empty()) { - const std::string bindText ("%%" + _lastEmittedText + "%%"); - constraint.where.And( WhereClause("(track.name like ? or track.release_name like ? or track.artist_name like ? or track.genre_list like ?)").bind(bindText).bind(bindText).bind(bindText).bind(bindText)); - } + std::vector values; + boost::algorithm::split(values, _lastEmittedText, boost::is_any_of(" "), boost::token_compress_on); - // else no constraint! + // For each part, do a global search on all searchable fields + BOOST_FOREACH(std::string value, values) + { + Database::SearchFilter::FieldValues likeMatch; + + likeMatch[Database::SearchFilter::Field::Artist].push_back(value); + likeMatch[Database::SearchFilter::Field::Release].push_back(value); + likeMatch[Database::SearchFilter::Field::Genre].push_back(value); + likeMatch[Database::SearchFilter::Field::Track].push_back(value); + + filter.likeMatches.push_back(likeMatch); + } + } } } // namespace UserInterface diff --git a/src/ui/audio/KeywordSearchFilter.hpp b/src/ui/audio/KeywordSearchFilter.hpp index a38f6382..5ff2df87 100644 --- a/src/ui/audio/KeywordSearchFilter.hpp +++ b/src/ui/audio/KeywordSearchFilter.hpp @@ -34,10 +34,10 @@ class KeywordSearchFilter : public Filter void setText(const std::string& text); // Set constraint on this filter - void refresh(const Constraint& constraint) {} + void refresh(Database::SearchFilter& filter) {} // Get constraints created by this filter - void getConstraint(Constraint& constraint); + void getConstraint(Database::SearchFilter& filter); private: diff --git a/src/ui/audio/TableFilter.cpp b/src/ui/audio/TableFilter.cpp index b2c156db..f062ebac 100644 --- a/src/ui/audio/TableFilter.cpp +++ b/src/ui/audio/TableFilter.cpp @@ -19,25 +19,41 @@ #include +#include "database/AudioTypes.hpp" + #include "logger/Logger.hpp" #include "TableFilter.hpp" namespace UserInterface { -TableFilter::TableFilter(Database::Handler& db, std::string table, std::string field, const Wt::WString& displayName, Wt::WContainerWidget* parent) +using namespace Database; + +TableFilter::TableFilter(Database::Handler& db, Database::SearchFilter::Field field, std::vector columnNames, Wt::WContainerWidget* parent) : Wt::WTableView( parent ), Filter(), _db(db), -_table(table), _field(field) { - _queryModel.setQuery( _db.getSession().query< ResultType >("select track." + _field + ", COUNT(DISTINCT track.id) from track GROUP BY track." + _field).orderBy("track." + _field)); - _queryModel.addColumn( "track." + _field, displayName); - _queryModel.addColumn( "COUNT(DISTINCT track.id)", "Tracks"); + SearchFilter filter; + + switch (field) + { + case Database::SearchFilter::Field::Artist: + Track::updateArtistQueryModel(_db.getSession(), _queryModel, filter, columnNames); + break; + case Database::SearchFilter::Field::Release: + Track::updateReleaseQueryModel(_db.getSession(), _queryModel, filter, columnNames); + break; + case Database::SearchFilter::Field::Genre: + Genre::updateGenreQueryModel(_db.getSession(), _queryModel, filter, columnNames); + break; + default: + break; + } this->setSelectionMode(Wt::ExtendedSelection); - this->setSortingEnabled(false); + this->setSortingEnabled(true); this->setAlternatingRowColors(true); this->setModel(&_queryModel); @@ -68,53 +84,37 @@ _field(field) void TableFilter::layoutSizeChanged (int width, int height) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "LAYOUT CHANGED, new width = " << width; - - 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 - 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 void -TableFilter::refresh(const Constraint& constraint) +TableFilter::refresh(SearchFilter& filter) { - SqlQuery sqlQuery; - - sqlQuery.select("track." + _field + ", COUNT(DISTINCT track.id)"); - sqlQuery.from().And( FromClause("track")) ; - sqlQuery.where().And(constraint.where); // Add constraint made by other filters - sqlQuery.groupBy().And( "track." + _field); // Add constraint made by other filters - - LMS_LOG(MOD_UI, SEV_DEBUG) << _table << ", generated query = '" << sqlQuery.get() << "'"; - - Wt::Dbo::Query query = _db.getSession().query( sqlQuery.get() ); - - query.orderBy(_table + "." + _field); - - BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "Binding value '" << bindArg << "'"; - query.bind(bindArg); + switch (_field) + { + case Database::SearchFilter::Field::Artist: + Track::updateArtistQueryModel(_db.getSession(), _queryModel, filter); + break; + case Database::SearchFilter::Field::Release: + Track::updateReleaseQueryModel(_db.getSession(), _queryModel, filter); + break; + case Database::SearchFilter::Field::Genre: + Genre::updateGenreQueryModel(_db.getSession(), _queryModel, filter); + break; + default: + break; } - - _queryModel.setQuery( query, true ); - - LMS_LOG(MOD_UI, SEV_DEBUG) << "Finish !"; } // Get constraint created by this filter void -TableFilter::getConstraint(Constraint& constraint) +TableFilter::getConstraint(SearchFilter& filter) { Wt::WModelIndexSet indexSet = this->selectedIndexes(); - // WHERE statement - WhereClause clause; - BOOST_FOREACH(Wt::WModelIndex index, indexSet) { if (!index.isValid()) @@ -122,14 +122,10 @@ TableFilter::getConstraint(Constraint& constraint) const ResultType& result = _queryModel.resultRow( index.row() ); - // Get the track part - std::string name(result.get<0>()); + std::string name = result.get<0>(); - clause.Or(_table + "." + _field + " = ?").bind(name); + filter.exactMatch[_field].push_back(name); } - - // Adding our WHERE clause - constraint.where.And( clause ); } } // namespace UserInterface diff --git a/src/ui/audio/TableFilter.hpp b/src/ui/audio/TableFilter.hpp index 2b5b3ca0..4f74dcd2 100644 --- a/src/ui/audio/TableFilter.hpp +++ b/src/ui/audio/TableFilter.hpp @@ -33,13 +33,13 @@ class TableFilter : public Wt::WTableView, public Filter { public: - TableFilter(Database::Handler& db, std::string table, std::string field, const Wt::WString& displayName, Wt::WContainerWidget* parent = 0); + TableFilter(Database::Handler& db, Database::SearchFilter::Field field, std::vector displayName, Wt::WContainerWidget* parent = 0); // Set constraints on this filter - void refresh(const Constraint& constraint); + void refresh(Database::SearchFilter& filter); // Get constraints created by this filter - void getConstraint(Constraint& constraint); + void getConstraint(Database::SearchFilter& filter); void layoutSizeChanged (int width, int height); @@ -52,10 +52,9 @@ class TableFilter : public Wt::WTableView, public Filter SigDoubleClicked _sigDoubleClicked; Database::Handler& _db; - const std::string _table; - const std::string _field; + Database::SearchFilter::Field _field; - // Name, track count, special value that means 'all' if set to 1 + // Name, track count typedef boost::tuple ResultType; Wt::Dbo::QueryModel< ResultType > _queryModel; diff --git a/src/ui/audio/TableFilterGenre.cpp b/src/ui/audio/TableFilterGenre.cpp deleted file mode 100644 index a90a6dee..00000000 --- a/src/ui/audio/TableFilterGenre.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2013 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 "logger/Logger.hpp" - -#include "TableFilterGenre.hpp" - -namespace UserInterface { - -TableFilterGenre::TableFilterGenre(Database::Handler& db, Wt::WContainerWidget* parent) -: Wt::WTableView( parent ), -Filter(), -_db(db) -{ - _queryModel.setQuery( _db.getSession().query< ResultType >("select genre.name, COUNT(DISTINCT track.id) from genre INNER JOIN track_genre ON track_genre.genre_id = genre.id INNER JOIN track ON track.id = track_genre.track_id").orderBy("genre.name").groupBy("genre.name").orderBy("genre.name")); - _queryModel.addColumn("genre.name", "Genre"); - _queryModel.addColumn("COUNT(DISTINCT track.id)", "tracks"); - - this->setSelectionMode(Wt::ExtendedSelection); - this->setSortingEnabled(false); - this->setAlternatingRowColors(true); - this->setModel(&_queryModel); - - this->selectionChanged().connect(this, &TableFilterGenre::emitUpdate); - - setLayoutSizeAware(true); - - _queryModel.setBatchSize(100); - - // If an item is double clicked, select and emit signal - this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt) - { - if (!idx.isValid()) - return; - - Wt::WModelIndexSet indexSet; - indexSet.insert(idx); - - this->setSelectedIndexes( indexSet ); - _sigDoubleClicked.emit( ); - }, std::placeholders::_1, std::placeholders::_2)); - -} - -void -TableFilterGenre::layoutSizeChanged (int width, int height) -{ - LMS_LOG(MOD_UI, SEV_DEBUG) << "LAYOUT CHANGED!"; - - /* TODO - std::size_t trackColumnSize = this->columnWidth(1).toPixels() + 30 ; - // Set the remaining size for the name column - this->setColumnWidth(0, width - 7 - trackColumnSize); - */ - -} - -// Set constraints on this filter -void -TableFilterGenre::refresh(const Constraint& constraint) -{ - SqlQuery sqlQuery; - - sqlQuery.select("genre.name, COUNT(DISTINCT track.id)"); - sqlQuery.from().And( std::string("genre INNER JOIN track_genre ON track_genre.genre_id = genre.id INNER JOIN track ON track.id = track_genre.track_id") ); - sqlQuery.where().And(constraint.where); // Add constraint made by other filters - sqlQuery.groupBy().And( std::string("genre.name") ); - - LMS_LOG(MOD_UI, SEV_DEBUG) << "genre, generated query = '" << sqlQuery.get() << "'"; - - Wt::Dbo::Query query = _db.getSession().query( sqlQuery.get() ); - - query.orderBy("genre.name"); - - BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "Binding value '" << bindArg << "'"; - query.bind(bindArg); - } - - _queryModel.setQuery( query, true ); - -} - -// Get constraint created by this filter -void -TableFilterGenre::getConstraint(Constraint& constraint) -{ - Wt::WModelIndexSet indexSet = this->selectedIndexes(); - - // WHERE statement - WhereClause clause; - - BOOST_FOREACH(Wt::WModelIndex index, indexSet) { - - if (!index.isValid()) - continue; - - const ResultType& result = _queryModel.resultRow( index.row() ); - - // Get the track part - std::string name(result.get<0>()); - - if (name == "") - clause.Or( std::string("track.genre_list = ?") ).bind(""); - else - clause.Or( std::string("track.genre_list LIKE ?") ).bind("%%" + name + "%%"); - } - - // Adding our WHERE clause - constraint.where.And( clause ); - -} - -} // namespace UserInterface - diff --git a/src/ui/audio/TableFilterGenre.hpp b/src/ui/audio/TableFilterGenre.hpp deleted file mode 100644 index 97ef5948..00000000 --- a/src/ui/audio/TableFilterGenre.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2013 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 . - */ - -#ifndef TABLE_FILTER_GENRE_HPP -#define TABLE_FILTER_GENRE_HPP - - -#include -#include - -#include "Filter.hpp" -#include "database/DatabaseHandler.hpp" - -namespace UserInterface { - -class TableFilterGenre : public Wt::WTableView, public Filter -{ - - public: - TableFilterGenre(Database::Handler& db, Wt::WContainerWidget* parent = 0); - - // Set constraints on this filter - void refresh(const Constraint& constraint); - - // Get constraints created by this filter - void getConstraint(Constraint& constraint); - - void layoutSizeChanged (int width, int height); - - typedef Wt::Signal SigDoubleClicked; - - SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; } - - protected: - - SigDoubleClicked _sigDoubleClicked; - - Database::Handler& _db; - - // genre_name, count - typedef boost::tuple ResultType; - Wt::Dbo::QueryModel< ResultType > _queryModel; - -}; - -} // namespace UserInterface - -#endif - diff --git a/src/ui/audio/TrackView.cpp b/src/ui/audio/TrackView.cpp index a57866c8..75bb62b4 100644 --- a/src/ui/audio/TrackView.cpp +++ b/src/ui/audio/TrackView.cpp @@ -33,18 +33,25 @@ TrackView::TrackView( Database::Handler& db, Wt::WContainerWidget* parent) : Wt::WTableView( parent ), _db(db) { - _queryModel.setQuery(_db.getSession().query("select track from track").orderBy("track.artist_name,track.date,track.release_name,track.disc_number,track.track_number")); - _queryModel.addColumn( "track.artist_name", "Artist" ); - _queryModel.addColumn( "track.release_name", "Album" ); - _queryModel.addColumn( "track.disc_number", "Disc #" ); - _queryModel.addColumn( "track.track_number", "Track #" ); - _queryModel.addColumn( "track.name", "Track" ); - _queryModel.addColumn( "track.duration", "Duration" ); - _queryModel.addColumn( "track.date", "Date" ); - _queryModel.addColumn( "track.original_date", "Original Date" ); - _queryModel.addColumn( "track.genre_list", "Genres" ); - _queryModel.setBatchSize(500); + static const std::vector columnNames = + { + "Artist", + "Album", + "Disc #", + "Track #", + "Track", + "Duration", + "Date", + "Original Date", + "Genres", + }; + + Database::SearchFilter filter; + + Database::Track::updateTracksQueryModel(_db.getSession(), _queryModel, filter, columnNames); + + _queryModel.setBatchSize(300); this->setSortingEnabled(true); this->setSelectionMode(Wt::ExtendedSelection); @@ -60,7 +67,6 @@ _db(db) this->setColumnWidth(6, 70); // Date this->setColumnWidth(7, 70); // Original Date this->setColumnWidth(8, 180); // Genres - // this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); // Duration display @@ -102,28 +108,9 @@ _db(db) // Set constraints created by parent filters void -TrackView::refresh(const Constraint& constraint) +TrackView::refresh(Database::SearchFilter& filter) { - - SqlQuery sqlQuery; - - sqlQuery.select( "track" ); - sqlQuery.from().And( FromClause("track")); - sqlQuery.where().And(constraint.where); - - LMS_LOG(MOD_UI, SEV_DEBUG) << "TRACK REQ = '" << sqlQuery.get() << "'"; - - Wt::Dbo::Query query = _db.getSession().query( sqlQuery.get() ); - - query.groupBy("track").orderBy("track.artist_name,track.date,track.release_name,track.disc_number,track.track_number"); - - BOOST_FOREACH(const std::string& bindArg, sqlQuery.where().getBindArgs()) { - LMS_LOG(MOD_UI, SEV_DEBUG) << "Binding value '" << bindArg << "'"; - query.bind(bindArg); - } - - _queryModel.setQuery( query, true ); - + Database::Track::updateTracksQueryModel(_db.getSession(), _queryModel, filter); } void @@ -138,10 +125,7 @@ TrackView::getSelectedTracks(std::vector& track_ids) if (!index.isValid()) continue; - const ResultType& result = _queryModel.resultRow( index.row() ); - - // Get the track part - Wt::Dbo::ptr track ( result.get<0>() ); + Database::Track::pointer track = _queryModel.resultRow( index.row() ); track_ids.push_back(track.id()); } @@ -177,10 +161,7 @@ TrackView::getTracks(std::vector& trackIds) for (int i = 0; i < _queryModel.rowCount(); ++i) { - const ResultType& result = _queryModel.resultRow( i ); - - // Get the track part - Wt::Dbo::ptr track ( result.get<0>() ); + Database::Track::pointer track = _queryModel.resultRow(i); trackIds.push_back(track.id()); } diff --git a/src/ui/audio/TrackView.hpp b/src/ui/audio/TrackView.hpp index 7b035bfc..6b0a24bf 100644 --- a/src/ui/audio/TrackView.hpp +++ b/src/ui/audio/TrackView.hpp @@ -38,10 +38,10 @@ class TrackView : public Wt::WTableView, public Filter // Filter interface // Set constraints created by parent filters - void refresh(const Constraint& constraint); + void refresh(Database::SearchFilter& filter); // Create constraints for child filters (N/A) - void getConstraint(Constraint& constraint) {} + void getConstraint(Database::SearchFilter& filter) {} // Get all the tracks that are currently selected void getSelectedTracks(std::vector& trackIds); @@ -64,7 +64,7 @@ class TrackView : public Wt::WTableView, public Filter Database::Handler& _db; - typedef boost::tuple ResultType; + typedef Database::Track::pointer ResultType; Wt::Dbo::QueryModel< ResultType > _queryModel; Wt::WTableView* _tableView;