diff --git a/conf/lms.conf.sample b/conf/lms.conf.sample index 2ca863b0..45adfb85 100644 --- a/conf/lms.conf.sample +++ b/conf/lms.conf.sample @@ -20,7 +20,7 @@ ui = { enable = true; resources = { - docroot = "/usr/share/Wt/" + docroot = "/var/lms/docroot" approot = "/var/lms/approot" } diff --git a/src/database/Playlist.cpp b/src/database/Playlist.cpp new file mode 100644 index 00000000..ba312b0c --- /dev/null +++ b/src/database/Playlist.cpp @@ -0,0 +1,38 @@ +/* + * 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 "AudioTypes.hpp" + +namespace Database { + +Playlist::Playlist() +: _public(false) +{ + +} + +Playlist::Playlist(std::string name, Wt::Dbo::ptr user, bool isPublic) +: _public(isPublic), + _user(user) +{ + +} + +} // namespace Database diff --git a/src/database/PlaylistEntry.cpp b/src/database/PlaylistEntry.cpp new file mode 100644 index 00000000..b0b151a1 --- /dev/null +++ b/src/database/PlaylistEntry.cpp @@ -0,0 +1,39 @@ +/* + * 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 "AudioTypes.hpp" + +namespace Database { + +PlaylistEntry::PlaylistEntry() +: _position(0) +{ + +} + +PlaylistEntry::PlaylistEntry(Wt::Dbo::ptr track, Wt::Dbo::ptr playlist) +: _position(0), + _track(track), + _playlist(playlist) +{ + +} + +} // namespace Database diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 415d12c7..a4328edc 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -76,6 +76,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, boost::filesystem::p bootstrapTheme->setResponsive(true); setTheme(bootstrapTheme); + useStyleSheet("css/lms.css"); + // Add a resource bundle messageResourceBundle().use(appRoot() + "templates"); diff --git a/src/ui/audio/Audio.cpp b/src/ui/audio/Audio.cpp index 89331c57..5bb9ab17 100644 --- a/src/ui/audio/Audio.cpp +++ b/src/ui/audio/Audio.cpp @@ -24,14 +24,13 @@ #include #include #include +#include #include "logger/Logger.hpp" #include "TableFilter.hpp" #include "TableFilterGenre.hpp" #include "KeywordSearchFilter.hpp" -#include "TrackView.hpp" -#include "PlayQueue.hpp" #include "Audio.hpp" @@ -40,41 +39,37 @@ namespace UserInterface { Audio::Audio(SessionData& sessionData, Wt::WContainerWidget* parent) : Wt::WContainerWidget(parent), _db(sessionData.getDatabaseHandler()), -_mediaPlayer(nullptr) +_mediaPlayer(nullptr), +_trackView(nullptr), +_playQueue(nullptr) { - Wt::WGridLayout *mainLayout = new Wt::WGridLayout(); this->setLayout(mainLayout); // Filters Wt::WHBoxLayout *filterLayout = new Wt::WHBoxLayout(); - { - TableFilterGenre *filter = new TableFilterGenre(_db); - filterLayout->addWidget(filter); - _filterChain.addFilter(filter); - } - { - TableFilter *filter = new TableFilter(_db, "track", "artist_name"); - filterLayout->addWidget(filter); - _filterChain.addFilter(filter); - } - { - TableFilter *filter = new TableFilter(_db, "track", "release_name"); - filterLayout->addWidget(filter); - _filterChain.addFilter(filter); - } + TableFilterGenre *filterGenre = new TableFilterGenre(_db); + filterLayout->addWidget(filterGenre); + _filterChain.addFilter(filterGenre); + + TableFilter *filterArtist = new TableFilter(_db, "track", "artist_name", "Artist"); + filterLayout->addWidget(filterArtist); + _filterChain.addFilter(filterArtist); + + TableFilter *filterRelease = new TableFilter(_db, "track", "release_name", "Release"); + filterLayout->addWidget(filterRelease); + _filterChain.addFilter(filterRelease); mainLayout->addLayout(filterLayout, 0, 0); // ENDOF(Filters) - // TODO ADD controls (Play, add) // TODO ADD some stats (nb files, total duration, etc.) Wt::WVBoxLayout* trackLayout = new Wt::WVBoxLayout(); - TrackView* trackView = new TrackView(_db); - trackLayout->addWidget(trackView, 1); + _trackView = new TrackView(_db); + trackLayout->addWidget(_trackView, 1); Wt::WHBoxLayout* trackControls = new Wt::WHBoxLayout(); @@ -90,9 +85,9 @@ _mediaPlayer(nullptr) mainLayout->addLayout(trackLayout, 1, 0); - _filterChain.addFilter(trackView); + _filterChain.addFilter(_trackView); - PlayQueue *playQueue = new PlayQueue(_db); + _playQueue = new PlayQueue(_db); // Playlist/PlayQueue { @@ -103,12 +98,49 @@ _mediaPlayer(nullptr) Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout(); - playlistControls->addWidget(new Wt::WPushButton("Playlist")); - playlistControls->addWidget(new Wt::WText("Duration:")); + Wt::WPushButton *saveBtn = new Wt::WPushButton("Save"); + playlistControls->addWidget(saveBtn); + Wt::WPushButton *loadBtn = new Wt::WPushButton("Load"); + playlistControls->addWidget(loadBtn); + + Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); + playlistControls->addWidget(upBtn); + Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); + playlistControls->addWidget(downBtn); + Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); + playlistControls->addWidget(delBtn); + + delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected); + upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp); + downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown); + + playlistControls->addWidget(new Wt::WText("Duration:"), 1); + + // Load menu + { + Wt::WPopupMenu *popup = new Wt::WPopupMenu(); + + popup->addItem("Metal"); + popup->addItem("Test"); + + loadBtn->setMenu(popup); + } + + // Save Menu + { + Wt::WPopupMenu *popup = new Wt::WPopupMenu(); + + popup->addItem("New"); + popup->addSeparator(); + popup->addItem("Metal"); + popup->addItem("Test"); + + saveBtn->setMenu(popup); + } playQueueLayout->addLayout(playlistControls); - playQueueLayout->addWidget( playQueue, 1); + playQueueLayout->addWidget( _playQueue, 1); mainLayout->addLayout(playQueueLayout, 0, 1, 2, 1); } @@ -122,54 +154,27 @@ _mediaPlayer(nullptr) // Double click on track // Set the selected tracks to the play queue - trackView->trackDoubleClicked().connect(std::bind([=] () - { - std::vector trackIds; - trackView->getSelectedTracks(trackIds); + _trackView->trackDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddSelectedTracks)); - playQueue->clear(); - playQueue->addTracks(trackIds); - - playQueue->play(); - - })); + // Double click on artist + // Set the selected tracks to the play queue + filterArtist->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks)); + filterRelease->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks)); + filterGenre->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks)); // Play button // Set the selected tracks to the play queue - playBtn->clicked().connect(std::bind([=] () - { - std::vector trackIds; - trackView->getSelectedTracks(trackIds); - - // If nothing is selected, get the whole track list - if (trackIds.empty()) - trackView->getTracks(trackIds); - - playQueue->clear(); - playQueue->addTracks(trackIds); - - playQueue->play(); - })); + playBtn->clicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddSelectedTracks)); // Add Button // Add the selected tracks at the end of the play queue - addBtn->clicked().connect(std::bind([=] () - { - std::vector trackIds; - trackView->getSelectedTracks(trackIds); + addBtn->clicked().connect(this, &Audio::addSelectedTracks); - // If nothing is selected, get the whole track list - if (trackIds.empty()) - trackView->getTracks(trackIds); + _playQueue->playTrack().connect(this, &Audio::playTrack); - playQueue->addTracks(trackIds); - })); - - playQueue->playTrack().connect(this, &Audio::playTrack); - - _mediaPlayer->playbackEnded().connect(playQueue, &PlayQueue::handlePlaybackComplete); - _mediaPlayer->playNext().connect(playQueue, &PlayQueue::playNext); - _mediaPlayer->playPrevious().connect(playQueue, &PlayQueue::playPrevious); + _mediaPlayer->playbackEnded().connect(_playQueue, &PlayQueue::handlePlaybackComplete); + _mediaPlayer->playNext().connect(_playQueue, &PlayQueue::playNext); + _mediaPlayer->playPrevious().connect(_playQueue, &PlayQueue::playPrevious); } void @@ -178,6 +183,55 @@ Audio::search(const std::string& searchText) _filterChain.searchKeyword(searchText); } +void +Audio::playSelectedTracks(PlayQueueAddType addType) +{ + int firstTrackPos = 0; + std::vector trackIds; + + switch(addType) + { + case PlayQueueAddAllTracks: + // Play all the tracks, from the first one + firstTrackPos = 0; + _trackView->getTracks(trackIds); + break; + case PlayQueueAddSelectedTracks: + // If nothing or only ONE selected, get them all + if (_trackView->getNbSelectedTracks() <= 1) + { + // Start to play at the selected track, if any + firstTrackPos = _trackView->getFirstSelectedTrackPosition(); + _trackView->getTracks(trackIds); + } + else + { + // Play all the selected tracks, frm the first one + firstTrackPos = 0; + _trackView->getSelectedTracks(trackIds); + } + break; + } + + _playQueue->clear(); + _playQueue->addTracks(trackIds); + + _playQueue->play(firstTrackPos); +} + +void +Audio::addSelectedTracks(void) +{ + std::vector trackIds; + _trackView->getSelectedTracks(trackIds); + + // If nothing is selected, get the whole track list + if (trackIds.empty()) + _trackView->getTracks(trackIds); + + _playQueue->addTracks(trackIds); +} + void Audio::playTrack(boost::filesystem::path p) { diff --git a/src/ui/audio/Audio.hpp b/src/ui/audio/Audio.hpp index 757be352..465bd8fa 100644 --- a/src/ui/audio/Audio.hpp +++ b/src/ui/audio/Audio.hpp @@ -27,6 +27,8 @@ #include "common/SessionData.hpp" #include "AudioMediaPlayer.hpp" +#include "TrackView.hpp" +#include "PlayQueue.hpp" #include "FilterChain.hpp" @@ -45,11 +47,22 @@ class Audio : public Wt::WContainerWidget void playTrack(boost::filesystem::path p); + enum PlayQueueAddType + { + PlayQueueAddAllTracks, + PlayQueueAddSelectedTracks, + }; + + void playSelectedTracks(PlayQueueAddType addType); + void addSelectedTracks(); + void handlePlaylistSelected(Wt::WString name); Database::Handler& _db; AudioMediaPlayer* _mediaPlayer; + TrackView* _trackView; + PlayQueue* _playQueue; FilterChain _filterChain; diff --git a/src/ui/audio/AudioMediaPlayer.cpp b/src/ui/audio/AudioMediaPlayer.cpp index e53111ef..ce91170f 100644 --- a/src/ui/audio/AudioMediaPlayer.cpp +++ b/src/ui/audio/AudioMediaPlayer.cpp @@ -63,11 +63,13 @@ AudioMediaPlayer::AudioMediaPlayer( Wt::WContainerWidget *parent) nextBtn->clicked().connect(std::bind([=] () { + _mediaPlayer->stop(); _playNext.emit(); })); prevBtn->clicked().connect(std::bind([=] () { + _mediaPlayer->stop(); _playPrevious.emit(); })); } diff --git a/src/ui/audio/PlayQueue.cpp b/src/ui/audio/PlayQueue.cpp index 67191329..3d2b74ff 100644 --- a/src/ui/audio/PlayQueue.cpp +++ b/src/ui/audio/PlayQueue.cpp @@ -17,15 +17,64 @@ * along with LMS. If not, see . */ +#include + +#include "logger/Logger.hpp" + #include "PlayQueue.hpp" +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 + 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 + } + } +} + namespace UserInterface { +static const int trackIdInvalid = -1; + +class PlayQueueItemDelegate : public Wt::WItemDelegate +{ + public: + PlayQueueItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent), _selectedRowId(trackIdInvalid) {} + + void setSelectedRowId(int rowId) { _selectedRowId = rowId; } + + Wt::WWidget* update(Wt::WWidget *widget, const Wt::WModelIndex &index, Wt::WFlags< Wt::ViewItemRenderFlag > flags) + { + Wt::WWidget* res = Wt::WItemDelegate::update(widget, index, flags); + + if (res && index.isValid() && index.row() == _selectedRowId) + res->toggleStyleClass("playqueue-playing", true); + + return res; + } + + private: + + int _selectedRowId; +}; + + PlayQueue::PlayQueue(Database::Handler& db, Wt::WContainerWidget* parent) : Wt::WTableView(parent), _db(db), -_playedId(-1) +_playedId(trackIdInvalid) { _model = new Wt::WStandardItemModel(0, 4, this); @@ -45,6 +94,9 @@ _playedId(-1) this->setColumnHidden(0, true); + _itemDelegate = new PlayQueueItemDelegate(); + this->setItemDelegate(_itemDelegate); + this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt) { if (!idx.isValid()) @@ -64,9 +116,9 @@ _playedId(-1) } void -PlayQueue::play(void) +PlayQueue::play(int trackId) { - readTrack(0); + readTrack(trackId); } void @@ -99,23 +151,21 @@ PlayQueue::clear(void) _model->removeRows(0, _model->rowCount()); // Reset play id - _playedId = -1; + _playedId = trackIdInvalid; } void PlayQueue::handlePlaybackComplete(void) { // Play the next track - if (_playedId != -1) - { + if (_playedId != trackIdInvalid) readTrack(_playedId + 1); - } } void PlayQueue::playNext(void) { - readTrack( _playedId != -1 ? _playedId + 1 : 0); + readTrack( _playedId != trackIdInvalid ? _playedId + 1 : 0); } void @@ -136,15 +186,141 @@ PlayQueue::readTrack(int rowId) Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); if (track) { - _playedId = rowId; + setPlayingTrackId(rowId); + _sigTrackPlay.emit(track->getPath()); - break; + + return; } ++rowId; } + } +void +PlayQueue::setPlayingTrackId(int newRowId) +{ + int oldRowId = _playedId; + _playedId = newRowId; + + _itemDelegate->setSelectedRowId(newRowId); + + // 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 (oldRowId >= 0) + modelForceRefreshDataRow(_model, oldRowId); + + if (newRowId >= 0) + modelForceRefreshDataRow(_model, newRowId); +} + +void +PlayQueue::delSelected(void) +{ + int minId = _model->rowCount(); + Wt::WModelIndexSet indexSet = this->selectedIndexes(); + + BOOST_REVERSE_FOREACH(Wt::WModelIndex index, indexSet) + { + _model->removeRow(index.row()); + if (index.row() < minId) + minId = index.row(); + } + + // If the current played track is removed, make sure to unselect it + _itemDelegate->setSelectedRowId(trackIdInvalid); + + renumber(minId, _model->rowCount() - 1); +} + +void +PlayQueue::moveSelectedUp(void) +{ + int minId = _model->rowCount(); + int maxId = 0; + + Wt::WModelIndexSet indexSet = this->selectedIndexes(); + Wt::WModelIndexSet newIndexSet; + + // ordered from up to down + BOOST_FOREACH(Wt::WModelIndex index, indexSet) + { + // Do nothing if the first selected index is on the top + if (index.row() == 0) + return; + + // TODO optimize for blocks + swapRows(_model, index.row() - 1, index.row()); + + if (index.row() - 1 < minId) + minId = index.row() - 1; + + if (index.row() > maxId) + maxId = index.row(); + + // Update playing status + if (_playedId == index.row()) + setPlayingTrackId(_playedId - 1); + else if (_playedId == index.row() - 1) + setPlayingTrackId(_playedId + 1); + + newIndexSet.insert( _model->index(index.row() - 1, 0)); + } + + this->setSelectedIndexes( newIndexSet ); + + renumber(minId, maxId); +} + + +void +PlayQueue::moveSelectedDown(void) +{ + int minId = _model->rowCount(); + int maxId = 0; + + Wt::WModelIndexSet indexSet = this->selectedIndexes(); + Wt::WModelIndexSet newIndexSet; + + // ordered from up to down + BOOST_REVERSE_FOREACH(Wt::WModelIndex index, indexSet) + { + // Do nothing if the last selected index is the last one + if (index.row() == _model->rowCount() - 1) + return; + + // TODO optimize for blocks + swapRows(_model, index.row(), index.row() + 1); + + if (index.row() < minId) + minId = index.row(); + + if (index.row() + 1 > maxId) + maxId = index.row() + 1; + + // Update playing status + if (_playedId == index.row()) + setPlayingTrackId(_playedId + 1); + else if (_playedId == index.row() + 1) + setPlayingTrackId(_playedId - 1); + + newIndexSet.insert( _model->index(index.row() + 1, 0)); + } + + this->setSelectedIndexes( newIndexSet ); + + renumber(minId, maxId); +} + +void +PlayQueue::renumber(int firstId, int lastId) +{ + for (int i = firstId; i <= lastId; ++i) + _model->setData( i, 1, i + 1); +} + + } // namespace UserInterface diff --git a/src/ui/audio/PlayQueue.hpp b/src/ui/audio/PlayQueue.hpp index 49a040a2..d73c17c7 100644 --- a/src/ui/audio/PlayQueue.hpp +++ b/src/ui/audio/PlayQueue.hpp @@ -28,6 +28,8 @@ namespace UserInterface { +class PlayQueueItemDelegate; + class PlayQueue : public Wt::WTableView { public: @@ -37,10 +39,16 @@ class PlayQueue : public Wt::WTableView void clear(void); - void play(void); // Play the queue from the beginning + // Play functions + void play(int trackId = 0); // Play the queue void playNext(void); // Play the next track void playPrevious(void); // Play the previous track + // List manipulations + void delSelected(void); + void moveSelectedUp(void); + void moveSelectedDown(void); + // Signals Wt::Signal< boost::filesystem::path >& playTrack() { return _sigTrackPlay; } @@ -52,6 +60,8 @@ class PlayQueue : public Wt::WTableView private: void readTrack(int rowId); + void setPlayingTrackId(int newRowId); + void renumber(int firstId, int lastId); Wt::Signal< boost::filesystem::path > _sigTrackPlay; @@ -59,6 +69,8 @@ class PlayQueue : public Wt::WTableView Wt::WStandardItemModel* _model; + PlayQueueItemDelegate* _itemDelegate; + int _playedId; }; diff --git a/src/ui/audio/TableFilter.cpp b/src/ui/audio/TableFilter.cpp index 196efa7a..4c5fbf6d 100644 --- a/src/ui/audio/TableFilter.cpp +++ b/src/ui/audio/TableFilter.cpp @@ -25,7 +25,7 @@ namespace UserInterface { -TableFilter::TableFilter(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent) +TableFilter::TableFilter(Database::Handler& db, std::string table, std::string field, const Wt::WString& displayName, Wt::WContainerWidget* parent) : Wt::WTableView( parent ), Filter(), _db(db), @@ -33,8 +33,8 @@ _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, field); - _queryModel.addColumn( "COUNT(DISTINCT track.id)", "tracks"); + _queryModel.addColumn( "track." + _field, displayName); + _queryModel.addColumn( "COUNT(DISTINCT track.id)", "Tracks"); this->setSelectionMode(Wt::ExtendedSelection); this->setSortingEnabled(false); @@ -46,6 +46,21 @@ _field(field) 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 diff --git a/src/ui/audio/TableFilter.hpp b/src/ui/audio/TableFilter.hpp index 76644ff5..2b5b3ca0 100644 --- a/src/ui/audio/TableFilter.hpp +++ b/src/ui/audio/TableFilter.hpp @@ -33,7 +33,7 @@ class TableFilter : public Wt::WTableView, public Filter { public: - TableFilter(Database::Handler& db, std::string table, std::string field, Wt::WContainerWidget* parent = 0); + TableFilter(Database::Handler& db, std::string table, std::string field, const Wt::WString& displayName, Wt::WContainerWidget* parent = 0); // Set constraints on this filter void refresh(const Constraint& constraint); @@ -43,8 +43,14 @@ class TableFilter : public Wt::WTableView, public Filter void layoutSizeChanged (int width, int height); + typedef Wt::Signal SigDoubleClicked; + + SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; } + protected: + SigDoubleClicked _sigDoubleClicked; + Database::Handler& _db; const std::string _table; const std::string _field; diff --git a/src/ui/audio/TableFilterGenre.cpp b/src/ui/audio/TableFilterGenre.cpp index 6ad84c07..a90a6dee 100644 --- a/src/ui/audio/TableFilterGenre.cpp +++ b/src/ui/audio/TableFilterGenre.cpp @@ -44,6 +44,20 @@ _db(db) 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 diff --git a/src/ui/audio/TableFilterGenre.hpp b/src/ui/audio/TableFilterGenre.hpp index b3968d44..97ef5948 100644 --- a/src/ui/audio/TableFilterGenre.hpp +++ b/src/ui/audio/TableFilterGenre.hpp @@ -43,8 +43,14 @@ class TableFilterGenre : public Wt::WTableView, public Filter void layoutSizeChanged (int width, int height); + typedef Wt::Signal SigDoubleClicked; + + SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; } + protected: + SigDoubleClicked _sigDoubleClicked; + Database::Handler& _db; // genre_name, count diff --git a/src/ui/audio/TrackView.cpp b/src/ui/audio/TrackView.cpp index d89522d0..a57866c8 100644 --- a/src/ui/audio/TrackView.cpp +++ b/src/ui/audio/TrackView.cpp @@ -149,6 +149,27 @@ TrackView::getSelectedTracks(std::vector& track_ids) LMS_LOG(MOD_UI, SEV_DEBUG) << "Getting all selected tracks DONE..."; } +std::size_t +TrackView::getNbSelectedTracks(void) +{ + return this->selectedIndexes().size(); +} + +int +TrackView::getFirstSelectedTrackPosition(void) +{ + Wt::WModelIndexSet indexSet = this->selectedIndexes(); + BOOST_FOREACH(Wt::WModelIndex index, indexSet) + { + if (!index.isValid()) + continue; + + return index.row(); + } + + return 0; +} + void TrackView::getTracks(std::vector& trackIds) { diff --git a/src/ui/audio/TrackView.hpp b/src/ui/audio/TrackView.hpp index 0c492257..7b035bfc 100644 --- a/src/ui/audio/TrackView.hpp +++ b/src/ui/audio/TrackView.hpp @@ -43,9 +43,13 @@ class TrackView : public Wt::WTableView, public Filter // Create constraints for child filters (N/A) void getConstraint(Constraint& constraint) {} - // Get all the tracks that are currently selected - void getSelectedTracks(std::vector& track_ids); + void getSelectedTracks(std::vector& trackIds); + + std::size_t getNbSelectedTracks(void); + + // Get the first position of the selected tracks (0 if nothing selected) + int getFirstSelectedTrackPosition(void); // Get all the tracks void getTracks(std::vector& track_ids); diff --git a/src/ui/docroot/css/lms.css b/src/ui/docroot/css/lms.css new file mode 100644 index 00000000..48f18b91 --- /dev/null +++ b/src/ui/docroot/css/lms.css @@ -0,0 +1,5 @@ + +.playqueue-playing { + font-weight: bold; +} +