[UI] Added a playqueue on mobile version, minor visual changes on desktop version

This commit is contained in:
emeric
2016-04-07 13:02:51 +02:00
parent 0d8516231a
commit 4c1dcbe9de
15 changed files with 485 additions and 65 deletions
+2 -2
View File
@@ -46,7 +46,7 @@ This command requires root privileges
## Running ## Running
Here is a command line example to run on port 5081: Here is a command line example to run on port 5081:
```sh ```sh
$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --approot=/usr/share/lms/approot --http-address 0.0.0.0 --http-port 5081 $ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images,/favicon.ico' --approot=/usr/share/lms/approot --http-address 0.0.0.0 --http-port 5081
``` ```
It is highly recommended to run LMS as a non root user. It is highly recommended to run LMS as a non root user.
The exectuable needs write accesses to the /var/lms/ directory. The exectuable needs write accesses to the /var/lms/ directory.
@@ -75,7 +75,7 @@ $ openssl req -x509 -out cert.pem -keyout privkey.pem -newkey rsa:4096
``` ```
Run (on port 5081): Run (on port 5081):
```sh ```sh
$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --approot=/usr/share/lms/approot --https-address 0.0.0.0 --https-port 5081 --ssl-certificate cert.pem --ssl-private-key privkey.pem --ssl-tmp-dh dh2048.pem $ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images,/favicon.ico' --approot=/usr/share/lms/approot --https-address 0.0.0.0 --https-port 5081 --ssl-certificate cert.pem --ssl-private-key privkey.pem --ssl-tmp-dh dh2048.pem
``` ```
Depending on your SSL parameters, you may be asked for the PEM passphrase to unlock the private key. Depending on your SSL parameters, you may be asked for the PEM passphrase to unlock the private key.
+28 -14
View File
@@ -356,15 +356,6 @@
</div> </div>
</message> </message>
<message id="mediaplayer-controls">
<div class="btn-group" role="group" aria-label="...">
${prev}
${play}
${pause}
${next}
</div>
</message>
<message id="mobile-search-title"> <message id="mobile-search-title">
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
@@ -426,7 +417,7 @@
</message> </message>
<!-- <!--
wa-track_search wa-track-search
-> wa-track-search-res -> wa-track-search-res
-> wa-track-search-res -> wa-track-search-res
-> ... -> ...
@@ -445,7 +436,7 @@
<div class="vertical-align"> <div class="vertical-align">
<div class="col-xs-2" style="margin-top:3px">${cover}</div> <div class="col-xs-2" style="margin-top:3px">${cover}</div>
<div class="col-xs-8">${artist-name}<br/>${track-name}</div> <div class="col-xs-8">${artist-name}<br/>${track-name}</div>
<div class="col-xs-2" style="margin-top:3px"><span class="badge">${btn}</span></div> <div class="col-xs-2">${play-btn}${add-btn}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -490,13 +481,36 @@
</message> </message>
<message id="wa-trackview-track"> <message id="wa-trackview-track">
<div class="menu-item list-group-item ">${<if-has-disc-num>}${disc-num}-${</if-has-disc-num>}${<if-has-track-num>}${track-num}. ${</if-has-track-num>}${track-name} <span class="badge">${play-btn}</span></div> <div class="menu-item list-group-item ">${<if-has-disc-num>}${disc-num}-${</if-has-disc-num>}${<if-has-track-num>}${track-num}. ${</if-has-track-num>}${track-name} ${play-btn}${add-btn}</div>
</message>
<!-- Playqueue view
!-->
<message id="wa-playqueue-view">
${controls}
<div class="menu-category list-group">
${contents}
</div>
</message>
<message id="wa-playqueue-controls">
${load}${save}${clear}${shuffle}${repeat}
</message>
<message id="wa-playqueue-track">
<div class="row">
<div class="vertical-align">
<div class="col-xs-2" style="margin-top:3px">${cover}</div>
<div class="col-xs-8">${artist-name}<br/>${track-name}</div>
<div class="col-xs-2" style="margin-top:3px">${play-btn}${del-btn}</div>
</div>
</div>
</message> </message>
<message id="wa-audio-player"> <message id="wa-audio-player">
${audio} ${audio}
${prev}${play-pause}${next} ${prev}${play-pause}${next}
<div class="mediaplayer-volume-container visible-lg"> <div class="mediaplayer-volume-container hidden-xs">
${volume} ${volume}
</div> </div>
<div class="mediaplayer-cover hidden-xs"> <div class="mediaplayer-cover hidden-xs">
@@ -510,7 +524,7 @@
${curtime}${seekbar}${duration} ${curtime}${seekbar}${duration}
</div> </div>
</div> </div>
${shuffle}${repeat} ${<if-has-shuffle>}${shuffle}${</if-has-shuffle>}${<if-has-repeat>}${repeat}${</if-has-repeat>}${<if-has-playqueue>}${playqueue}${</if-has-playqueue>}
</message> </message>
</messages> </messages>
+23
View File
@@ -129,6 +129,19 @@
writing-mode: bt-lr; writing-mode: bt-lr;
} }
.mobile-btn {
margin: 8px;
}
.mobile-btn-active {
color: #428bca;
}
.mobile-btn i {
cursor: pointer;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.mobile-search-title { .mobile-search-title {
font-weight: bold; font-weight: bold;
height: 32px; height: 32px;
@@ -198,3 +211,13 @@
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
.release-search-cover {
width: 512px;
min-width: 512px;
height: 512px;
min-height: 512px;
display: flex;
justify-content: space-between;
align-items: center;
}
+1
View File
@@ -35,6 +35,7 @@ lms_SOURCES = \
$(srcdir)/ui/audio/mobile/ArtistSearchView.cpp \ $(srcdir)/ui/audio/mobile/ArtistSearchView.cpp \
$(srcdir)/ui/audio/mobile/ArtistView.cpp \ $(srcdir)/ui/audio/mobile/ArtistView.cpp \
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \ $(srcdir)/ui/audio/mobile/MobileAudio.cpp \
$(srcdir)/ui/audio/mobile/MobilePlayQueue.cpp \
$(srcdir)/ui/audio/mobile/PreviewSearchView.cpp \ $(srcdir)/ui/audio/mobile/PreviewSearchView.cpp \
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \ $(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
$(srcdir)/ui/audio/mobile/ReleaseSearchView.cpp \ $(srcdir)/ui/audio/mobile/ReleaseSearchView.cpp \
+58 -30
View File
@@ -42,7 +42,6 @@ AudioPlayer::getBestEncoding() const
return Av::Encoding::MP3; return Av::Encoding::MP3;
} }
bool bool
AudioPlayer::loadTrack(Database::Track::id_type trackId) AudioPlayer::loadTrack(Database::Track::id_type trackId)
{ {
@@ -121,7 +120,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId)
return true; return true;
} }
AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) AudioPlayer::AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent)
: Wt::WTemplate(parent) : Wt::WTemplate(parent)
{ {
setTemplateText(Wt::WString::tr("wa-audio-player")); setTemplateText(Wt::WString::tr("wa-audio-player"));
@@ -154,42 +153,71 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
volumeSlider->setAttributeValue("orient", "vertical"); // firefox volumeSlider->setAttributeValue("orient", "vertical"); // firefox
bindWidget("volume", volumeSlider); bindWidget("volume", volumeSlider);
Wt::WText *repeatBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText); if (controls & ControlRepeat)
repeatBtn->addStyleClass("mediaplayer-btn");
bindWidget("repeat", repeatBtn);
repeatBtn->clicked().connect(std::bind([=] ()
{ {
if (repeatBtn->hasStyleClass("mediaplayer-btn-active")) setCondition("if-has-repeat", true);
Wt::WText *repeatBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
repeatBtn->addStyleClass("mediaplayer-btn hidden-xs");
bindWidget("repeat", repeatBtn);
repeatBtn->clicked().connect(std::bind([=] ()
{ {
repeatBtn->removeStyleClass("mediaplayer-btn-active"); if (repeatBtn->hasStyleClass("mediaplayer-btn-active"))
_loop.emit(false); {
} repeatBtn->removeStyleClass("mediaplayer-btn-active");
else _loop.emit(false);
{ }
repeatBtn->addStyleClass("mediaplayer-btn-active"); else
_loop.emit(true); {
} repeatBtn->addStyleClass("mediaplayer-btn-active");
})); _loop.emit(true);
}
}));
}
Wt::WText *shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText); if (controls & ControlPlayqueue)
shuffleBtn->addStyleClass("mediaplayer-btn");
bindWidget("shuffle", shuffleBtn);
shuffleBtn->clicked().connect(std::bind([=] ()
{ {
if (shuffleBtn->hasStyleClass("mediaplayer-btn-active")) setCondition("if-has-playqueue", true);
Wt::WText *playQueueBtn = new Wt::WText("<i class=\"fa fa-list-ul fa-2x\"></i>", Wt::XHTMLText);
bindWidget("playqueue", playQueueBtn);
playQueueBtn->addStyleClass("mediaplayer-btn");
playQueueBtn->clicked().connect(std::bind([=] ()
{ {
shuffleBtn->removeStyleClass("mediaplayer-btn-active"); if (playQueueBtn->hasStyleClass("mediaplayer-btn-active"))
_shuffle.emit(false); {
} playQueueBtn->removeStyleClass("mediaplayer-btn-active");
else _playQueue.emit(false);
}
else
{
playQueueBtn->addStyleClass("mediaplayer-btn-active");
_playQueue.emit(true);
}
}));
}
if (controls & ControlShuffle)
{
setCondition("if-has-shuffle", true);
Wt::WText *shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
shuffleBtn->addStyleClass("mediaplayer-btn hidden-xs");
bindWidget("shuffle", shuffleBtn);
shuffleBtn->clicked().connect(std::bind([=] ()
{ {
shuffleBtn->addStyleClass("mediaplayer-btn-active"); if (shuffleBtn->hasStyleClass("mediaplayer-btn-active"))
_shuffle.emit(true); {
} shuffleBtn->removeStyleClass("mediaplayer-btn-active");
})); _shuffle.emit(false);
}
else
{
shuffleBtn->addStyleClass("mediaplayer-btn-active");
_shuffle.emit(true);
}
}));
}
Wt::WText *prevBtn = new Wt::WText("<i class=\"fa fa-step-backward fa-2x\"></i>", Wt::XHTMLText); Wt::WText *prevBtn = new Wt::WText("<i class=\"fa fa-step-backward fa-2x\"></i>", Wt::XHTMLText);
prevBtn->addStyleClass("mediaplayer-btn hidden-xs"); prevBtn->addStyleClass("mediaplayer-btn");
bindWidget("prev", prevBtn); bindWidget("prev", prevBtn);
prevBtn->clicked().connect(std::bind([=] () prevBtn->clicked().connect(std::bind([=] ()
{ {
+14 -1
View File
@@ -32,7 +32,15 @@ class AudioPlayer : public Wt::WTemplate
{ {
public: public:
AudioPlayer(Wt::WContainerWidget *parent = 0); enum ControlFlags
{
ControlNone = 0,
ControlShuffle = 1 << 0,
ControlRepeat = 1 << 1,
ControlPlayqueue = 1 << 2,
};
AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent = 0);
Av::Encoding getBestEncoding() const; Av::Encoding getBestEncoding() const;
bool loadTrack(Database::Track::id_type trackId); bool loadTrack(Database::Track::id_type trackId);
@@ -43,6 +51,7 @@ class AudioPlayer : public Wt::WTemplate
Wt::Signal<void>& playPrevious() {return _playPrevious;} Wt::Signal<void>& playPrevious() {return _playPrevious;}
Wt::Signal<bool>& shuffle() {return _shuffle;} Wt::Signal<bool>& shuffle() {return _shuffle;}
Wt::Signal<bool>& loop() {return _loop;} Wt::Signal<bool>& loop() {return _loop;}
Wt::Signal<bool>& showPlayQueue() {return _playQueue;}
private: private:
@@ -52,6 +61,7 @@ class AudioPlayer : public Wt::WTemplate
Wt::Signal<void> _playPrevious; Wt::Signal<void> _playPrevious;
Wt::Signal<bool> _shuffle; Wt::Signal<bool> _shuffle;
Wt::Signal<bool> _loop; Wt::Signal<bool> _loop;
Wt::Signal<bool> _playQueue;
Wt::WAudio* _audio; Wt::WAudio* _audio;
Wt::WText* _trackCurTime; Wt::WText* _trackCurTime;
@@ -60,4 +70,7 @@ class AudioPlayer : public Wt::WTemplate
Wt::WImage* _cover; Wt::WImage* _cover;
}; };
constexpr AudioPlayer::ControlFlags operator|(AudioPlayer::ControlFlags f1, AudioPlayer::ControlFlags f2) { return AudioPlayer::ControlFlags(int(f1)|int(f2)); }
} // namespace UserInterface } // namespace UserInterface
+5 -5
View File
@@ -149,16 +149,16 @@ _playQueue(nullptr)
playlistBtn->setMenu(popupMain); playlistBtn->setMenu(popupMain);
} }
Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); Wt::WPushButton *upBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-up fa-lg\"></i>", Wt::XHTMLText);
upBtn->setStyleClass("btn-sm"); upBtn->setStyleClass("btn-sm");
playlistControls->addWidget(upBtn); playlistControls->addWidget(upBtn);
Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); Wt::WPushButton *downBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-down fa-lg\"></i>", Wt::XHTMLText);
downBtn->setStyleClass("btn-sm"); downBtn->setStyleClass("btn-sm");
playlistControls->addWidget(downBtn); playlistControls->addWidget(downBtn);
Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); Wt::WPushButton *delBtn = new Wt::WPushButton("<i class =\"fa fa-remove fa-lg\"></i>", Wt::XHTMLText);
delBtn->setStyleClass("btn-sm btn-warning"); delBtn->setStyleClass("btn-sm btn-warning");
playlistControls->addWidget(delBtn); playlistControls->addWidget(delBtn);
Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR"); Wt::WPushButton *clearBtn = new Wt::WPushButton("<i class =\"fa fa-trash fa-lg\"></i>", Wt::XHTMLText);
clearBtn->setStyleClass("btn-sm btn-danger"); clearBtn->setStyleClass("btn-sm btn-danger");
playlistControls->addWidget(clearBtn); playlistControls->addWidget(clearBtn);
@@ -180,7 +180,7 @@ _playQueue(nullptr)
_playQueue->select(CurrentUser()->getCurPlayingTrackPos()); _playQueue->select(CurrentUser()->getCurPlayingTrackPos());
} }
_mediaPlayer = new AudioPlayer(); _mediaPlayer = new AudioPlayer(AudioPlayer::ControlShuffle | AudioPlayer::ControlRepeat);
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4); mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
mainLayout->setRowStretch(1, 1); mainLayout->setRowStretch(1, 1);
+3 -3
View File
@@ -55,7 +55,7 @@ TableFilterGenre::TableFilterGenre(Wt::WContainerWidget* parent)
this->setLayoutSizeAware(true); this->setLayoutSizeAware(true);
_queryModel.setBatchSize(100); _queryModel.setBatchSize(150);
// If an item is double clicked, select and emit signal // If an item is double clicked, select and emit signal
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt) this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
@@ -134,7 +134,7 @@ TableFilterArtist::TableFilterArtist(Wt::WContainerWidget* parent)
this->setLayoutSizeAware(true); this->setLayoutSizeAware(true);
_queryModel.setBatchSize(100); _queryModel.setBatchSize(150);
// If an item is double clicked, select and emit signal // If an item is double clicked, select and emit signal
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt) this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
@@ -218,7 +218,7 @@ TableFilterRelease::TableFilterRelease(Wt::WContainerWidget* parent)
this->setLayoutSizeAware(true); this->setLayoutSizeAware(true);
_queryModel.setBatchSize(100); _queryModel.setBatchSize(150);
// If an item is double clicked, select and emit signal // If an item is double clicked, select and emit signal
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt) this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
+1 -1
View File
@@ -70,7 +70,7 @@ TrackView::TrackView(Wt::WContainerWidget* parent)
Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames); Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
_queryModel.setBatchSize(300); _queryModel.setBatchSize(500);
this->setSortingEnabled(true); this->setSortingEnabled(true);
this->setSelectionMode(Wt::ExtendedSelection); this->setSelectionMode(Wt::ExtendedSelection);
+43 -2
View File
@@ -37,6 +37,8 @@
#include "ArtistView.hpp" #include "ArtistView.hpp"
#include "ReleaseView.hpp" #include "ReleaseView.hpp"
#include "MobilePlayQueue.hpp"
#include "MobileAudio.hpp" #include "MobileAudio.hpp"
@@ -51,6 +53,7 @@ enum WidgetIdx
WidgetIdxSearchTrack = 3, WidgetIdxSearchTrack = 3,
WidgetIdxArtist = 4, WidgetIdxArtist = 4,
WidgetIdxRelease = 5, WidgetIdxRelease = 5,
WidgetIdxPlayQueue = 6,
}; };
using namespace Database; using namespace Database;
@@ -70,6 +73,8 @@ Audio::Audio(Wt::WContainerWidget *parent)
Wt::WStackedWidget *stack = new Wt::WStackedWidget(this); Wt::WStackedWidget *stack = new Wt::WStackedWidget(this);
PlayQueue *playQueue = new PlayQueue();
// Same order as WidgetIdxXXX // Same order as WidgetIdxXXX
stack->addWidget(new PreviewSearchView(_playQueueEvents)); stack->addWidget(new PreviewSearchView(_playQueueEvents));
stack->addWidget(new ArtistSearchView()); stack->addWidget(new ArtistSearchView());
@@ -77,6 +82,7 @@ Audio::Audio(Wt::WContainerWidget *parent)
stack->addWidget(new TrackSearchView(_playQueueEvents)); stack->addWidget(new TrackSearchView(_playQueueEvents));
stack->addWidget(new ArtistView()); stack->addWidget(new ArtistView());
stack->addWidget(new ReleaseView(_playQueueEvents)); stack->addWidget(new ReleaseView(_playQueueEvents));
stack->addWidget(playQueue);
wApp->internalPathChanged().connect(std::bind([=] (std::string path) wApp->internalPathChanged().connect(std::bind([=] (std::string path)
{ {
@@ -89,6 +95,7 @@ Audio::Audio(Wt::WContainerWidget *parent)
{ "/audio/search/track", WidgetIdxSearchTrack}, { "/audio/search/track", WidgetIdxSearchTrack},
{ "/audio/artist", WidgetIdxArtist}, { "/audio/artist", WidgetIdxArtist},
{ "/audio/release", WidgetIdxRelease}, { "/audio/release", WidgetIdxRelease},
{ "/audio/playqueue", WidgetIdxPlayQueue},
}; };
for (auto index : indexes) for (auto index : indexes)
@@ -102,14 +109,48 @@ Audio::Audio(Wt::WContainerWidget *parent)
Wt::WTemplate* footer = new Wt::WTemplate(this); Wt::WTemplate* footer = new Wt::WTemplate(this);
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer")); footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
AudioPlayer* audioPlayer = new AudioPlayer(); AudioPlayer* audioPlayer = new AudioPlayer(AudioPlayer::ControlPlayqueue);
footer->bindWidget("player", audioPlayer); footer->bindWidget("player", audioPlayer);
audioPlayer->showPlayQueue().connect(std::bind([=] (bool state)
{
if (state)
wApp->setInternalPath("/audio/playqueue", true);
}, std::placeholders::_1));
// Connect the audio player events to the playqueue
playQueue->playTrack().connect(std::bind([=] (Database::Track::id_type id)
{
audioPlayer->loadTrack(id);
}, std::placeholders::_1));
audioPlayer->playNext().connect(std::bind([=]
{
playQueue->playNext();
}));
audioPlayer->playPrevious().connect(std::bind([=]
{
playQueue->playPrevious();
}));
audioPlayer->playbackEnded().connect(std::bind([=]
{
playQueue->playNext();
}));
audioPlayer->shuffle().connect(std::bind(&PlayQueue::setShuffle, playQueue, std::placeholders::_1));
audioPlayer->loop().connect(std::bind(&PlayQueue::setLoop, playQueue, std::placeholders::_1));
// Connect the events to the player // Connect the events to the player
_playQueueEvents.trackPlay.connect(std::bind([=] (Database::Track::id_type id) _playQueueEvents.trackPlay.connect(std::bind([=] (Database::Track::id_type id)
{ {
audioPlayer->loadTrack(id); std::size_t pos = playQueue->addTrack(id);
playQueue->play(pos);
}, std::placeholders::_1)); }, std::placeholders::_1));
_playQueueEvents.trackAdd.connect(std::bind([=] (Database::Track::id_type id)
{
playQueue->addTrack(id);
}, std::placeholders::_1));
} }
} // namespace Mobile } // namespace Mobile
+206
View File
@@ -0,0 +1,206 @@
/*
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*/
#include <Wt/WTemplate>
#include <Wt/WImage>
#include <Wt/WText>
#include <Wt/WPushButton>
#include "logger/Logger.hpp"
#include "LmsApplication.hpp"
#include "MobilePlayQueue.hpp"
namespace UserInterface {
namespace Mobile {
PlayQueue::PlayQueue(Wt::WContainerWidget* parent)
: Wt::WContainerWidget(parent)
{
Wt::WTemplate* t = new Wt::WTemplate(this);
t->setTemplateText(Wt::WString::tr("wa-playqueue-view"));
_trackContainer = new Wt::WContainerWidget();
t->bindWidget("contents", _trackContainer);
Wt::WTemplate* controls = new Wt::WTemplate();
t->bindWidget("controls", controls);
controls->setTemplateText(Wt::WString::tr("wa-playqueue-controls"));
Wt::WText *loadBtn = new Wt::WText("<i class=\"fa fa-upload fa-2x\"></i>", Wt::XHTMLText);
controls->bindWidget("load", loadBtn);
loadBtn->setStyleClass("mobile-btn");
Wt::WText *saveBtn = new Wt::WText("<i class=\"fa fa-download fa-2x\"></i>", Wt::XHTMLText);
controls->bindWidget("save", saveBtn);
saveBtn->setStyleClass("mobile-btn");
Wt::WText *clearBtn = new Wt::WText("<i class=\"fa fa-remove fa-2x\"></i>", Wt::XHTMLText);
controls->bindWidget("clear", clearBtn);
clearBtn->setStyleClass("mobile-btn");
clearBtn->clicked().connect(std::bind([=]
{
clear();
}));
_shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
controls->bindWidget("shuffle", _shuffleBtn);
_shuffleBtn->setStyleClass("mobile-btn");
_shuffleBtn->clicked().connect(std::bind([=] ()
{
setShuffle(!_shuffle);
}));
_loopBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
controls->bindWidget("repeat", _loopBtn);
_loopBtn->setStyleClass("mobile-btn");
_loopBtn->clicked().connect(std::bind([=] ()
{
setLoop(!_loop);
}));
}
std::size_t
PlayQueue::addTrack(Database::Track::id_type id)
{
Wt::Dbo::Transaction transaction(DboSession());
Database::Track::pointer track = Database::Track::getById(DboSession(), id);
if (!track)
{
LMS_LOG(UI, INFO) << "No track found for id " << id;
return 0;
}
std::size_t trackPos = _trackIds.size();
_trackIds.push_back(id);
Wt::WTemplate* t = new Wt::WTemplate(_trackContainer);
t->setTemplateText(Wt::WString::tr("wa-playqueue-track"));
Wt::WImage *cover = new Wt::WImage();
t->bindWidget("cover", cover);
cover->setStyleClass ("center-block img-responsive");
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
t->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
t->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
t->bindWidget("play-btn", playBtn);
playBtn->addStyleClass("mobile-btn");
playBtn->clicked().connect(std::bind([=]
{
int pos = _trackContainer->indexOf(t);
play(pos);
}));
Wt::WText *delBtn = new Wt::WText("<i class=\"fa fa-remove fa-lg\"></i>", Wt::XHTMLText);
t->bindWidget("del-btn", delBtn);
delBtn->addStyleClass("mobile-btn");
delBtn->clicked().connect(std::bind([=]
{
int pos = _trackContainer->indexOf(t);
_trackContainer->removeWidget(t);
_trackIds.erase(_trackIds.begin() + pos);
}));
return trackPos;
}
void
PlayQueue::clear(void)
{
_trackIds.clear();
_trackContainer->clear();
_currentPos = 0;
}
void
PlayQueue::play(size_t pos)
{
if (_trackIds.empty() || pos >= _trackIds.size())
return;
_trackContainer->widget(_currentPos)->removeStyleClass("playqueue-playing");
_currentPos = pos;
_trackContainer->widget(_currentPos)->addStyleClass("playqueue-playing");
_sigTrackPlay.emit(_trackIds[pos]);
}
void
PlayQueue::playNext(void)
{
if (_currentPos == (_trackIds.size() - 1) && _loop)
play(0);
else
play(_currentPos + 1);
}
void
PlayQueue::playPrevious(void)
{
if (_currentPos == 0)
{
if (_loop)
{
play(_trackIds.size() - 1);
}
else
return;
}
else
play(_currentPos - 1);
}
void
PlayQueue::handlePlaybackComplete(void)
{
playNext();
}
void
PlayQueue::setShuffle(bool value)
{
_shuffle = value;
if (value)
_shuffleBtn->addStyleClass("mobile-btn-active");
else
_shuffleBtn->removeStyleClass("mobile-btn-active");
}
void
PlayQueue::setLoop(bool value)
{
_loop = value;
if (value)
_loopBtn->addStyleClass("mobile-btn-active");
else
_loopBtn->removeStyleClass("mobile-btn-active");
}
} // namespace Mobile
} // namespace UserInterface
+75
View File
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vector>
#include <Wt/WContainerWidget>
#include <Wt/WSignal>
#include "database/Types.hpp"
namespace UserInterface {
namespace Mobile {
class PlayQueue : public Wt::WContainerWidget
{
public:
PlayQueue(Wt::WContainerWidget* parent = 0);
// Contents
std::size_t addArtist(Database::Artist::id_type id);
std::size_t addRelease(Database::Release::id_type id);
std::size_t addTrack(Database::Track::id_type id);
void clear(void);
// Controls
void play(size_t pos); // Play the track at a given position
void playNext(void);
void playPrevious(void);
void setShuffle(bool enable); // true to enable shuffle
void setLoop(bool enable); // true to enable loop
// Signals
// Emitted when a song has to be played
Wt::Signal<Database::Track::id_type>& playTrack() { return _sigTrackPlay; }
// Slots
// Song has finished
void handlePlaybackComplete(void);
private:
bool _loop = false;
bool _shuffle = false;
std::size_t _currentPos = 0;
Wt::WText* _shuffleBtn = nullptr;
Wt::WText* _loopBtn = nullptr;
Wt::Signal<Database::Track::id_type> _sigTrackPlay;
std::vector<Database::Track::id_type> _trackIds;
Wt::WContainerWidget* _trackContainer;
};
} // namespace Mobile
} // namespace UserInterface
@@ -28,6 +28,7 @@ namespace Mobile {
struct PlayQueueEvents struct PlayQueueEvents
{ {
Wt::Signal<Database::Track::id_type> trackPlay; Wt::Signal<Database::Track::id_type> trackPlay;
Wt::Signal<Database::Track::id_type> trackAdd;
}; };
+13 -3
View File
@@ -177,13 +177,23 @@ ReleaseView::addResults(size_t nb)
trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText); trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
// TODO, display artist name for compilation releases? // TODO, display artist name for compilation releases?
Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText); Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
playBtn->setStyleClass("center-block"); // TODO move to CSS? trackRes->bindWidget("play-btn", playBtn);
playBtn->setStyleClass("mobile-btn");
playBtn->clicked().connect(std::bind([=] { playBtn->clicked().connect(std::bind([=] {
_events.trackPlay.emit(track.id()); _events.trackPlay.emit(track.id());
})); }));
trackRes->bindWidget("play-btn", playBtn); Wt::WText *addBtn = new Wt::WText("<i class=\"fa fa-plus fa-lg\"></i>", Wt::XHTMLText);
trackRes->bindWidget("add-btn", addBtn);
addBtn->setStyleClass("mobile-btn");
addBtn->clicked().connect(std::bind([=] {
_events.trackAdd.emit(track.id());
}));
_nbTracks++; _nbTracks++;
} }
+12 -4
View File
@@ -89,19 +89,27 @@ TrackSearch::addResults(size_t nb)
res->setTemplateText(Wt::WString::tr("wa-track-search-res")); res->setTemplateText(Wt::WString::tr("wa-track-search-res"));
Wt::WImage *cover = new Wt::WImage(); Wt::WImage *cover = new Wt::WImage();
res->bindWidget("cover", cover);
cover->setStyleClass ("center-block img-responsive"); cover->setStyleClass ("center-block img-responsive");
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64)); cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
res->bindWidget("cover", cover);
res->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText); res->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
res->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText); res->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText); Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
playBtn->setStyleClass("center-block"); // TODO move to CSS? res->bindWidget("play-btn", playBtn);
playBtn->setStyleClass("mobile-btn");
playBtn->clicked().connect(std::bind([=] { playBtn->clicked().connect(std::bind([=] {
_events.trackPlay.emit(track.id()); _events.trackPlay.emit(track.id());
})); }));
res->bindWidget("btn", playBtn); Wt::WText *addBtn = new Wt::WText("<i class=\"fa fa-plus fa-lg\"></i>", Wt::XHTMLText);
res->bindWidget("add-btn", addBtn);
addBtn->setStyleClass("mobile-btn");
addBtn->clicked().connect(std::bind([=] {
_events.trackAdd.emit(track.id());
}));
} }
if (moreResults) if (moreResults)