New UI: first step
This commit is contained in:
+7
-1
@@ -17,7 +17,13 @@ lms_images_DATA = \
|
||||
docroot/images/unknown-artist.jpg
|
||||
|
||||
lms_approot_DATA = \
|
||||
approot/templates.xml
|
||||
approot/artists.xml \
|
||||
approot/artist.xml \
|
||||
approot/messages.xml \
|
||||
approot/releases.xml \
|
||||
approot/release.xml \
|
||||
approot/templates.xml \
|
||||
approot/tracks.xml
|
||||
|
||||
sysconf_DATA = \
|
||||
$(top_srcdir)/conf/lms.conf
|
||||
|
||||
@@ -4,8 +4,7 @@ LMS is a self-hosted media streaming software, released under the GPLv3 license.
|
||||
It allows you to access your music and your videos using an http(s) web interface.
|
||||
|
||||
## Features
|
||||
- Winamp-like interface, suited for large media collections
|
||||
- Audio/Video transcode for maximum interoperability and low bandwith requirements
|
||||
- Audio transcode for maximum interoperability and low bandwith requirements
|
||||
- User management
|
||||
- Playlist support
|
||||
- MusicBrainzID support to handle duplicated artist and release names
|
||||
@@ -14,20 +13,13 @@ Tags are automatically created to facilitate music searching, using:
|
||||
- Multi genre information from metadata
|
||||
- High-level information from AcousticBrainz
|
||||
|
||||
Please note LMS is still under development and will gain more features in the future:
|
||||
- Radio function
|
||||
- Interface suited for mobile devices
|
||||
- Video support
|
||||
- Subsonic and/or Ampache API
|
||||
|
||||
LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and memory are limited.
|
||||
Please note some media files may require significant CPU usage to be transcoded.
|
||||
|
||||
## Dependencies
|
||||
### Debian
|
||||
|
||||
```sh
|
||||
$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev libcurl-dev libcurlpp-dev libconfig++-dev
|
||||
$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev libcurl-dev libcurlpp-dev libconfig++-dev ffmpeg
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="template-artist">
|
||||
<div class="page-header">
|
||||
<h2>${name}</h2>
|
||||
</div>
|
||||
${releases class="list-group"}
|
||||
</message>
|
||||
|
||||
<message id="template-artist-entry">
|
||||
<div class="media list-group-item">
|
||||
<div class="pull-left media-middle">
|
||||
${cover class="media-object"}
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading">${name}</h4>
|
||||
</div>
|
||||
${<if-has-various-artists>}${tr:msg-various-artists}${</if-has-various-artists>}
|
||||
<h4>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</h4>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="template-artists">
|
||||
<div class="page-header">
|
||||
<h2>${tr:msg-artists}</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
</div>
|
||||
</div>
|
||||
${artists class="list-group"}
|
||||
</message>
|
||||
|
||||
<message id="template-artists-entry">
|
||||
<div class="list-group-item">
|
||||
<span class="badge">${nb-release}</span>
|
||||
${name}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="msg-various-artists">Various artists</message>
|
||||
<message id="msg-artists">Artists</message>
|
||||
<message id="msg-releases">Albums</message>
|
||||
<message id="msg-tracks">Tracks</message>
|
||||
<message id="msg-playlist">Playlist</message>
|
||||
<message id="msg-filter-add">Add filter</message>
|
||||
<message id="msg-search-placeholder">Search...</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="template-release">
|
||||
${cover}
|
||||
<div class="page-header">
|
||||
<h2>${name}</h2>
|
||||
<h4>${artist-name}</h4>
|
||||
<h4>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</h4>
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
${tracks}
|
||||
</message>
|
||||
|
||||
<message id="template-release-entry">
|
||||
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}. ${</if-has-track-number>}
|
||||
${<if-has-artist>}${artist-name} - ${</if-has-artist>}
|
||||
${name}
|
||||
${play-btn}${add-btn}
|
||||
</message>
|
||||
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="template-releases">
|
||||
<div class="page-header">
|
||||
<h2>${tr:msg-releases}</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
</div>
|
||||
</div>
|
||||
${releases class="list-group"}
|
||||
</message>
|
||||
|
||||
<message id="template-releases-entry">
|
||||
<div class="media list-group-item">
|
||||
<div class="pull-left media-middle">
|
||||
${cover class="media-object"}
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading">${release-name}</h4>
|
||||
${artist-name}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+17
-9
@@ -2,15 +2,23 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Wt.WNavigationBar.template">
|
||||
<div>
|
||||
${collapse-button}
|
||||
<div class="navbar-header">
|
||||
${expand-button}
|
||||
${title-link}
|
||||
${search}
|
||||
</div>
|
||||
${contents}
|
||||
<message id="template-main">
|
||||
${navbar-top class="main-nav"}
|
||||
${contents class="container"}
|
||||
</message>
|
||||
|
||||
<message id="template-home">
|
||||
${tr:msg-home-welcome}
|
||||
</message>
|
||||
|
||||
<message id="template-explore">
|
||||
${filters}
|
||||
${contents}
|
||||
</message>
|
||||
|
||||
<message id="template-filters">
|
||||
<div class="form-group">
|
||||
${add-filter}${filters}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="template-tracks">
|
||||
<div class="page-header">
|
||||
<h2>${tr:msg-tracks}</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
</div>
|
||||
</div>
|
||||
${tracks class="list-group"}
|
||||
</message>
|
||||
|
||||
<message id="template-tracks-entry">
|
||||
<div class="media list-group-item">
|
||||
<div class="pull-left media-middle">
|
||||
${cover class="media-object"}
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading">${name}</h4>
|
||||
</div>
|
||||
${<if-has-release>}${release-name}${</if-has-release>}
|
||||
${<if-has-artist>}${artist-name}${</if-has-artist>}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -1,14 +1,5 @@
|
||||
.main-nav {
|
||||
margin-bottom: 0px;
|
||||
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.Wt-hrh2 {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.Wt-vrh2 {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.playqueue {
|
||||
@@ -198,26 +189,3 @@
|
||||
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5)
|
||||
}
|
||||
|
||||
.release_truncated-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.release_img-responsive {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.release-search-cover {
|
||||
width: 512px;
|
||||
min-width: 512px;
|
||||
height: 512px;
|
||||
min-height: 512px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
+9
-28
@@ -5,7 +5,7 @@ lms_SOURCES = \
|
||||
$(srcdir)/av/AvInfo.cpp \
|
||||
$(srcdir)/av/AvTranscoder.cpp \
|
||||
$(srcdir)/cover/CoverArtGrabber.cpp \
|
||||
$(srcdir)/database/Artist.cpp \
|
||||
$(srcdir)/database/DbArtist.cpp \
|
||||
$(srcdir)/database/DatabaseHandler.cpp \
|
||||
$(srcdir)/database/MediaDirectory.cpp \
|
||||
$(srcdir)/database/Playlist.cpp \
|
||||
@@ -24,41 +24,22 @@ lms_SOURCES = \
|
||||
$(srcdir)/image/Image.cpp \
|
||||
$(srcdir)/metadata/AvFormat.cpp \
|
||||
$(srcdir)/metadata/TagLibParser.cpp \
|
||||
$(srcdir)/ui/ArtistView.cpp \
|
||||
$(srcdir)/ui/ArtistsView.cpp \
|
||||
$(srcdir)/ui/Explore.cpp \
|
||||
$(srcdir)/ui/Filters.cpp \
|
||||
$(srcdir)/ui/HomeView.cpp \
|
||||
$(srcdir)/ui/LmsApplication.cpp \
|
||||
$(srcdir)/ui/ReleasesView.cpp \
|
||||
$(srcdir)/ui/ReleaseView.cpp \
|
||||
$(srcdir)/ui/TracksView.cpp \
|
||||
$(srcdir)/ui/auth/LmsAuth.cpp \
|
||||
$(srcdir)/ui/audio/AudioPlayer.cpp \
|
||||
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
|
||||
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
|
||||
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
|
||||
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
|
||||
$(srcdir)/ui/audio/desktop/TableFilter.cpp \
|
||||
$(srcdir)/ui/audio/desktop/TrackView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ArtistSearch.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ArtistSearchView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ArtistView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \
|
||||
$(srcdir)/ui/audio/mobile/MobilePlayQueue.cpp \
|
||||
$(srcdir)/ui/audio/mobile/PreviewSearchView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ReleaseSearchView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/ReleaseView.cpp \
|
||||
$(srcdir)/ui/audio/mobile/TrackSearch.cpp \
|
||||
$(srcdir)/ui/audio/mobile/TrackSearchView.cpp \
|
||||
$(srcdir)/ui/common/DirectoryValidator.cpp \
|
||||
$(srcdir)/ui/common/InputRange.cpp \
|
||||
$(srcdir)/ui/common/LineEdit.cpp \
|
||||
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
|
||||
$(srcdir)/ui/resource/ImageResource.cpp \
|
||||
$(srcdir)/ui/resource/TranscodeResource.cpp \
|
||||
$(srcdir)/ui/settings/Settings.cpp \
|
||||
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsDatabaseFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
|
||||
$(srcdir)/ui/settings/SettingsMediaDirectoryFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsUserFormView.cpp \
|
||||
$(srcdir)/ui/settings/SettingsUsers.cpp \
|
||||
$(srcdir)/utils/Config.cpp \
|
||||
$(srcdir)/utils/Logger.cpp \
|
||||
$(srcdir)/utils/Path.cpp \
|
||||
|
||||
@@ -178,11 +178,7 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re
|
||||
if (!release || release->isNone())
|
||||
return std::vector<Image::Image>();
|
||||
|
||||
// Get the first track of the release
|
||||
std::vector<Track::pointer> tracks = Track::getByFilter(session,
|
||||
SearchFilter::ById(SearchFilter::Field::Release, releaseId),
|
||||
-1, 1 /* limit result size */);
|
||||
|
||||
std::vector<Track::pointer> tracks = release->getTracks();
|
||||
if (tracks.empty())
|
||||
return std::vector<Image::Image>();
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ Handler::createConnectionPool(boost::filesystem::path p)
|
||||
|
||||
connection->executeSql("pragma journal_mode=WAL");
|
||||
|
||||
// connection->setProperty("show-queries", "true");
|
||||
connection->setProperty("show-queries", "true");
|
||||
return new Wt::Dbo::FixedSqlConnectionPool(connection, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
typedef Wt::Dbo::dbo_default_traits::IdType id_type;
|
||||
typedef Wt::Auth::Dbo::UserDatabase<AuthInfo> UserDatabase;
|
||||
|
||||
// Session living class handling the database and the login
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 "Types.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
Artist::Artist(const std::string& name, const std::string& MBID)
|
||||
: _name(std::string(name, 0 , _maxNameLength)),
|
||||
_MBID(MBID)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByName(Wt::Dbo::Session& session, const std::string& name)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.find<Artist>().where("name = ?").bind( std::string(name, 0, _maxNameLength) );
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
|
||||
{
|
||||
return session.find<Artist>().where("mbid = ?").bind(mbid);
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getById(Wt::Dbo::Session& session, Artist::id_type id)
|
||||
{
|
||||
return session.find<Artist>().where("id = ?").bind(id);
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
|
||||
{
|
||||
return session.add(new Artist(name, MBID));
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getNone(Wt::Dbo::Session& session)
|
||||
{
|
||||
std::vector<pointer> res = getByName(session, "<None>");
|
||||
if (res.empty())
|
||||
return create(session, "<None>");
|
||||
|
||||
return res.front();
|
||||
}
|
||||
|
||||
bool
|
||||
Artist::isNone() const
|
||||
{
|
||||
return _name == "<None>";
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Wt::Dbo::Session& session, int offset, int size)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = session.find<Artist>().orderBy("LOWER(name)").offset(offset).limit(size);
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAllOrphans(Wt::Dbo::Session& session)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("SELECT DISTINCT a FROM artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT a FROM artist a";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN track t ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY a.name";
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query = session.query<Artist::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusters,
|
||||
const std::vector<std::string> keywords,
|
||||
int offset, int size, bool& moreResults)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords).limit(size).offset(offset);
|
||||
|
||||
auto res = std::vector<pointer>(collection.begin(), collection.end());
|
||||
|
||||
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release> >
|
||||
Artist::getReleases(const std::vector<id_type>& clusterIds) const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id";
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
where.And(WhereClause("a.id = ?")).bind(std::to_string(id()));
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
// TODO order
|
||||
oss << " ORDER BY t.date,r.name";
|
||||
|
||||
Wt::Dbo::Query<Release::pointer> query = session()->query<Release::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
}
|
||||
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Release> > res = query;
|
||||
|
||||
return std::vector< Wt::Dbo::ptr<Release> > (res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Database
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
#ifndef _DB_ARTIST_HPP_
|
||||
#define _DB_ARTIST_HPP_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo>
|
||||
#include <Wt/Dbo/QueryModel>
|
||||
|
||||
#include "SearchFilter.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Track;
|
||||
class Cluster;
|
||||
class Release;
|
||||
|
||||
class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
{
|
||||
public:
|
||||
|
||||
typedef Wt::Dbo::ptr<Artist> pointer;
|
||||
typedef Wt::Dbo::dbo_traits<Artist>::IdType id_type;
|
||||
|
||||
Artist() {}
|
||||
Artist(const std::string& name, const std::string& MBID = "");
|
||||
|
||||
// Accessors
|
||||
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
|
||||
static pointer getById(Wt::Dbo::Session& session, id_type id);
|
||||
static pointer getNone(Wt::Dbo::Session& session); // Special entry
|
||||
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusters, // at least one track that belongs to these clusters
|
||||
const std::vector<std::string> keywords, // name must match all of these keywords
|
||||
int offset,
|
||||
int size,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1);
|
||||
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // No track related
|
||||
|
||||
// Accessors
|
||||
std::string getName(void) const { return _name; }
|
||||
std::string getMBID(void) const { return _MBID; }
|
||||
|
||||
// Get the releases that have at least one track for this artist + belongs to optonal cluster filters
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::vector<id_type>& clusterIds = std::vector<id_type>()) const;
|
||||
|
||||
void setMBID(std::string mbid) { _MBID = mbid; }
|
||||
|
||||
// Create
|
||||
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
|
||||
|
||||
bool isNone(void) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "artist");
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks of this artist
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
#endif
|
||||
+87
-59
@@ -82,68 +82,61 @@ Release::getAll(Wt::Dbo::Session& session, int offset, int size)
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllOrphans(Wt::Dbo::Session& session)
|
||||
{
|
||||
Wt::Dbo::collection<Release::pointer> res = session.query< Wt::Dbo::ptr<Release> >("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL");
|
||||
Wt::Dbo::collection<Release::pointer> res = session.query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Release::pointer>
|
||||
Release::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
getQuery(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusterIds,
|
||||
const std::vector<std::string> keywords)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
WhereClause where;
|
||||
|
||||
Wt::Dbo::Query<pointer> query
|
||||
= session.query<pointer>("SELECT r FROM release r INNER JOIN artist a ON a.id = t.artist_id INNER JOIN track t ON t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id " + sqlQuery.where().get()).groupBy("r.id").orderBy("r.name");
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT r FROM release r";
|
||||
|
||||
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
|
||||
query.bind(bindArg);
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<Release::UIQueryResult>
|
||||
Release::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
|
||||
// TODO DATE of RELEASE
|
||||
Wt::Dbo::Query<UIQueryResult> query
|
||||
= session.query<UIQueryResult>("SELECT r.id, r.name, t.date, COUNT(DISTINCT t.id) FROM release r INNER JOIN track t ON t.release_id = r.id INNER JOIN artist a ON a.id = t.artist_id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id " + sqlQuery.where().get()).groupBy("r.id").orderBy("r.name");
|
||||
|
||||
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
void
|
||||
Release::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
|
||||
{
|
||||
Wt::Dbo::Query<UIQueryResult> query = getUIQuery(session, filter);
|
||||
|
||||
model.setQuery(query, columnNames.empty() ? true : false);
|
||||
|
||||
// TODO do something better
|
||||
if (columnNames.size() == 3)
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
model.addColumn( "r.name", columnNames[0]);
|
||||
model.addColumn( "t.date", columnNames[1]);
|
||||
model.addColumn( "COUNT(DISTINCT t.id)", columnNames[2]);
|
||||
oss << " INNER JOIN track t ON t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.And(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY r.name";
|
||||
|
||||
Wt::Dbo::Query<Release::pointer> query = session.query<Release::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
|
||||
Release::getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusterIds,
|
||||
const std::vector<std::string> keywords,
|
||||
int offset, int size, bool& moreResults)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = getQuery(session, filter).limit(size).offset(offset);
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords).limit(size).offset(offset);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreResults)
|
||||
{
|
||||
auto res = getByFilter(session, filter, offset, size + 1);
|
||||
auto res = std::vector<pointer>(collection.begin(), collection.end());
|
||||
|
||||
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
|
||||
{
|
||||
@@ -156,28 +149,63 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
boost::optional<int>
|
||||
Release::getReleaseYear(bool original) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// TODO something better
|
||||
auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1);
|
||||
Wt::Dbo::collection<boost::posix_time::ptime> times = session()->query<boost::posix_time::ptime>(
|
||||
std::string("SELECT ") + (original ? "t.original_date" : "t.date") + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("t.date")
|
||||
.bind(this->id());
|
||||
|
||||
if (tracks.empty())
|
||||
return 0;
|
||||
/* various dates, no date */
|
||||
if (times.empty() || times.size() > 1)
|
||||
return boost::none;
|
||||
|
||||
boost::gregorian::date date;
|
||||
|
||||
if (original)
|
||||
date = tracks.front()->getOriginalDate().date();
|
||||
else
|
||||
date = tracks.front()->getDate().date();
|
||||
boost::gregorian::date date = times.front().date();
|
||||
|
||||
if (date.is_special())
|
||||
return 0;
|
||||
return boost::none;
|
||||
|
||||
return date.year();
|
||||
return boost::make_optional<int>(date.year());
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
Release::getArtists() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT DISTINCT a FROM artist a INNER JOIN release r ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id")
|
||||
.where("r.id = ?")
|
||||
.bind(id());
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Artist>>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
Release::hasVariousArtists() const
|
||||
{
|
||||
// TODO optimize
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
Release::getTracks() const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> res = session()->query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?")
|
||||
.orderBy("t.disc_number,t.track_number")
|
||||
.bind(id());
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(res.begin(), res.end());
|
||||
}
|
||||
} // namespace Database
|
||||
|
||||
+20
-18
@@ -17,11 +17,11 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _DB_RELEASE_HPP_
|
||||
#define _DB_RELEASE_HPP_
|
||||
#pragma once
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <Wt/Dbo/Dbo>
|
||||
#include <Wt/Dbo/QueryModel>
|
||||
|
||||
#include "SearchFilter.hpp"
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Database
|
||||
|
||||
class Track;
|
||||
class Release;
|
||||
class Artist;
|
||||
|
||||
class Release : public Wt::Dbo::Dbo<Release>
|
||||
{
|
||||
@@ -46,30 +47,34 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
|
||||
static pointer getById(Wt::Dbo::Session& session, id_type id);
|
||||
static pointer getNone(Wt::Dbo::Session& session); // Special entry
|
||||
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session);
|
||||
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // no track related
|
||||
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset, int size);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusters, // at least one track that belongs to these clusters
|
||||
const std::vector<std::string> keywords, // name must match all of these keywords
|
||||
int offset,
|
||||
int size,
|
||||
bool& moreExpected);
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks() const;
|
||||
|
||||
// Create
|
||||
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
|
||||
|
||||
// Utility functions
|
||||
int getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
|
||||
boost::optional<int> getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
|
||||
|
||||
// MVC models for the user interface
|
||||
// ID, Release name, year, track counts
|
||||
typedef boost::tuple<id_type, std::string, boost::posix_time::ptime, int> UIQueryResult;
|
||||
static Wt::Dbo::Query<UIQueryResult> getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
|
||||
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
|
||||
|
||||
|
||||
// Accessosrs
|
||||
// Accessors
|
||||
std::string getName() const { return _name; }
|
||||
std::string getMBID() const { return _MBID; }
|
||||
bool isNone(void) const;
|
||||
boost::posix_time::time_duration getDuration(void) const;
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists() const;
|
||||
bool hasVariousArtists() const;
|
||||
|
||||
void setMBID(std::string mbid) { _MBID = mbid; }
|
||||
|
||||
template<class Action>
|
||||
@@ -82,8 +87,6 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
}
|
||||
|
||||
private:
|
||||
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
@@ -94,5 +97,4 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
|
||||
} // namespace Database
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -24,98 +24,15 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
static std::ostream& operator<<(std::ostream& ost, const std::vector< Wt::Dbo::dbo_default_traits::IdType>& ids)
|
||||
void
|
||||
SearchFilter::operator+=(const SearchFilter& filter)
|
||||
{
|
||||
const char *sep = "";
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
ost << sep << std::to_string(id);
|
||||
sep = ",";
|
||||
}
|
||||
|
||||
return ost;
|
||||
}
|
||||
|
||||
SqlQuery generatePartialQuery(SearchFilter& filter)
|
||||
SqlQuery
|
||||
SearchFilter::generatePartialQuery()
|
||||
{
|
||||
SqlQuery sqlQuery;
|
||||
|
||||
// Process name like parameters
|
||||
for (auto nameLikeMatches : filter.nameLikeMatch)
|
||||
{
|
||||
WhereClause likeWhereClause;
|
||||
|
||||
for (auto nameLikeMatch : nameLikeMatches)
|
||||
{
|
||||
switch (nameLikeMatch.first)
|
||||
{
|
||||
case SearchFilter::Field::Artist:
|
||||
for (const std::string& name : nameLikeMatch.second)
|
||||
likeWhereClause.Or( WhereClause("a.name LIKE ?") ).bind("%%" + name + "%%");
|
||||
break;
|
||||
|
||||
case SearchFilter::Field::Release:
|
||||
for (const std::string& name : nameLikeMatch.second)
|
||||
likeWhereClause.Or( WhereClause("r.name LIKE ?") ).bind("%%" + name + "%%");
|
||||
break;
|
||||
|
||||
case SearchFilter::Field::Cluster:
|
||||
for (const std::string& name : nameLikeMatch.second)
|
||||
likeWhereClause.Or( WhereClause("c.name LIKE ?") ).bind("%%" + name + "%%");
|
||||
break;
|
||||
|
||||
case SearchFilter::Field::Track:
|
||||
for (const std::string& name : nameLikeMatch.second)
|
||||
likeWhereClause.Or( WhereClause("t.name LIKE ?") ).bind("%%" + name + "%%");
|
||||
break;
|
||||
}
|
||||
}
|
||||
sqlQuery.where().And( likeWhereClause );
|
||||
}
|
||||
|
||||
// Process id exact match parameters
|
||||
// Id list may be long, we do not bind the parameters
|
||||
// Safe since we already known these are just ints
|
||||
for (auto idMatch : filter.idMatch)
|
||||
{
|
||||
WhereClause idWhereClause;
|
||||
|
||||
switch (idMatch.first)
|
||||
{
|
||||
case SearchFilter::Field::Artist:
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (" << idMatch.second << ")";
|
||||
idWhereClause.Or(oss.str());
|
||||
}
|
||||
break;
|
||||
case SearchFilter::Field::Release:
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (" << idMatch.second << ")";
|
||||
idWhereClause.Or(oss.str());
|
||||
}
|
||||
break;
|
||||
case SearchFilter::Field::Cluster:
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "c.id IN (" << idMatch.second << ")";
|
||||
idWhereClause.Or(oss.str());
|
||||
}
|
||||
break;
|
||||
case SearchFilter::Field::Track:
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (" << idMatch.second << ")";
|
||||
idWhereClause.Or(oss.str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
sqlQuery.where().And( idWhereClause );
|
||||
}
|
||||
|
||||
return sqlQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,71 +31,37 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
typedef Wt::Dbo::dbo_default_traits::IdType id_type;
|
||||
|
||||
class SearchFilter
|
||||
{
|
||||
public:
|
||||
|
||||
enum class Field {
|
||||
Artist, // artist
|
||||
Release, // release
|
||||
Track, // track
|
||||
Cluster, // cluster
|
||||
};
|
||||
|
||||
typedef std::vector<std::map<Field, std::vector<std::string> > > NameLikeMatchType;
|
||||
typedef std::map<Field, std::vector< Wt::Dbo::dbo_default_traits::IdType> > IdMatchType;
|
||||
typedef int id_type;
|
||||
|
||||
SearchFilter() {}
|
||||
|
||||
// Helpers
|
||||
static SearchFilter IdMatch( const IdMatchType& _idMatch )
|
||||
{
|
||||
return SearchFilter(_idMatch);
|
||||
}
|
||||
static SearchFilter Artist(std::string name) {return SearchFilter();}
|
||||
static SearchFilter Artist(id_type id) {return SearchFilter();}
|
||||
|
||||
static SearchFilter NameLikeMatch( const NameLikeMatchType& _nameLikeMatch )
|
||||
{
|
||||
return SearchFilter(_nameLikeMatch);
|
||||
}
|
||||
static SearchFilter Release(std::string name) {return SearchFilter();}
|
||||
static SearchFilter Release(id_type id) {return SearchFilter();}
|
||||
|
||||
// Single Field ID match
|
||||
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
|
||||
{
|
||||
return SearchFilter({{field, {id} }});
|
||||
}
|
||||
static SearchFilter Track(std::string name) {return SearchFilter();}
|
||||
|
||||
// Single Field Name match OR
|
||||
static SearchFilter ByNameOr(Field field, std::vector<std::string> keywords)
|
||||
{
|
||||
return NameLikeMatch({{{field, keywords}}});
|
||||
}
|
||||
static SearchFilter Cluster(id_type id) {return SearchFilter();}
|
||||
|
||||
// Single Field Name match AND
|
||||
static SearchFilter ByNameAnd(Field field, std::vector<std::string> keywords)
|
||||
{
|
||||
NameLikeMatchType nameLikeMatch;
|
||||
// Combine search filters by add operation
|
||||
// Caution: multiple filters on different artist/release/track
|
||||
// values may lead to empty results
|
||||
void operator+=(const SearchFilter& filter);
|
||||
|
||||
for (auto keyword : keywords)
|
||||
nameLikeMatch.push_back({{{field, {keyword}}}});
|
||||
|
||||
return NameLikeMatch(nameLikeMatch);
|
||||
}
|
||||
|
||||
// The filter is a AND of the following conditions:
|
||||
|
||||
// ((Field1.name LIKE STR1-1 OR Field1.name LIKE STR1-2 ...) OR (Field2.name LIKE STR2-1 OR Field2.name LIKE STR2-2 ...) ...
|
||||
NameLikeMatchType nameLikeMatch;
|
||||
|
||||
// (Field1.id IN (ID1-1,ID1-2 ... ) AND (Field2.id IN (ID2-1,ID2-2 ... ) ...
|
||||
IdMatchType idMatch;
|
||||
SqlQuery generatePartialQuery();
|
||||
|
||||
private:
|
||||
|
||||
SearchFilter(const NameLikeMatchType& _nameLikeMatch) : nameLikeMatch(_nameLikeMatch) {}
|
||||
SearchFilter(const IdMatchType& _idMatch) : idMatch(_idMatch) {}
|
||||
};
|
||||
|
||||
SqlQuery generatePartialQuery(SearchFilter& filter);
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
+62
-71
@@ -108,29 +108,42 @@ Track::getClusters(void) const
|
||||
return clusters;
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query< Track::pointer >
|
||||
Track::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
getQuery(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusterIds,
|
||||
const std::vector<std::string> keywords)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
WhereClause where;
|
||||
|
||||
Wt::Dbo::Query<pointer> query
|
||||
= session.query<pointer>( "SELECT t FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get()).groupBy("t.id").orderBy("a.name,t.date,r.name,t.disc_number,t.track_number");
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT t FROM track t";
|
||||
|
||||
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
|
||||
query.bind(bindArg);
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("t.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
return query;
|
||||
}
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
Wt::Dbo::Query< Track::UIQueryResult >
|
||||
Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
WhereClause clusterClause;
|
||||
|
||||
Wt::Dbo::Query<UIQueryResult> query
|
||||
= session.query<UIQueryResult>( "SELECT t.id, a.name, r.name, t.disc_number, t.track_number, t.name, t.duration, t.date, t.original_date, t.genre_list FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get()).groupBy("t.id").orderBy("a.name,t.date,r.name,t.disc_number,t.track_number");
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.And(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY t.name";
|
||||
|
||||
Wt::Dbo::Query<Track::pointer> query = session.query<Track::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
@@ -139,7 +152,7 @@ Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
Track::StatsQueryResult
|
||||
Track::getStats(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
SqlQuery sqlQuery = filter.generatePartialQuery();
|
||||
|
||||
Wt::Dbo::Query<StatsQueryResult> query = session.query<StatsQueryResult>( "SELECT COUNT(\"id\"), SUM(\"dur\") FROM (SELECT t.id as \"id\", t.duration as \"dur\" FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get() + " GROUP BY t.id)");
|
||||
|
||||
@@ -151,17 +164,14 @@ Track::getStats(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
|
||||
Track::getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusterIds,
|
||||
const std::vector<std::string> keywords,
|
||||
int offset, int size, bool& moreResults)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = getQuery(session, filter).limit(size).offset(offset);
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords).limit(size).offset(offset);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreResults)
|
||||
{
|
||||
auto res = getByFilter(session, filter, offset, size + 1);
|
||||
auto res = std::vector<pointer>(collection.begin(), collection.end());
|
||||
|
||||
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
|
||||
{
|
||||
@@ -174,26 +184,28 @@ Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, i
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
Track::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
|
||||
boost::optional<std::size_t>
|
||||
Track::getTrackNumber(void) const
|
||||
{
|
||||
Wt::Dbo::Query< UIQueryResult > query = getUIQuery(session, filter);
|
||||
model.setQuery(query, columnNames.empty() ? true : false);
|
||||
return (_trackNumber > 0) ? boost::make_optional<std::size_t>(_trackNumber) : boost::none;
|
||||
}
|
||||
|
||||
// TODO do something better
|
||||
if (columnNames.size() == 9)
|
||||
{
|
||||
model.addColumn( "a.name", columnNames[0] );
|
||||
model.addColumn( "r.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] );
|
||||
}
|
||||
boost::optional<std::size_t>
|
||||
Track::getTotalTrackNumber(void) const
|
||||
{
|
||||
return (_totalTrackNumber > 0) ? boost::make_optional<std::size_t>(_totalTrackNumber) : boost::none;
|
||||
}
|
||||
|
||||
boost::optional<std::size_t>
|
||||
Track::getDiscNumber(void) const
|
||||
{
|
||||
return (_discNumber > 0) ? boost::make_optional<std::size_t>(_discNumber) : boost::none;
|
||||
}
|
||||
|
||||
boost::optional<std::size_t>
|
||||
Track::getTotalDiscNumber(void) const
|
||||
{
|
||||
return (_totalDiscNumber > 0) ? boost::make_optional<std::size_t>(_totalDiscNumber) : boost::none;
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +233,13 @@ Cluster::get(Wt::Dbo::Session& session, std::string type, std::string name)
|
||||
return session.find<Cluster>().where("type = ?").where("name = ?").bind( std::string(type, 0, _maxTypeLength)).bind( std::string(name, 0, _maxNameLength));
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Cluster::getByType(Wt::Dbo::Session& session, std::string type)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = session.find<Cluster>().where("type = ?").bind( std::string(type, 0, _maxTypeLength)).orderBy("name");
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::getNone(Wt::Dbo::Session& session)
|
||||
{
|
||||
@@ -252,7 +271,7 @@ Cluster::remove(Wt::Dbo::Session& session, std::string type)
|
||||
Wt::Dbo::Query<Cluster::pointer>
|
||||
Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
SqlQuery sqlQuery = filter.generatePartialQuery();
|
||||
|
||||
Wt::Dbo::Query<pointer> query
|
||||
= session.query<pointer>( "SELECT g FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN artist a ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.id = t_c.track_id " + sqlQuery.where().get()).groupBy("c.name").orderBy("c.name");
|
||||
@@ -263,34 +282,6 @@ Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<Cluster::UIQueryResult>
|
||||
Cluster::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
SqlQuery sqlQuery = generatePartialQuery(filter);
|
||||
|
||||
Wt::Dbo::Query<UIQueryResult> query
|
||||
= session.query<UIQueryResult>( "SELECT c.id, c.name, COUNT(DISTINCT t.id) FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN artist a ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.id = t_c.track_id " + sqlQuery.where().get()).groupBy("c.name").orderBy("c.name");
|
||||
|
||||
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
|
||||
{
|
||||
Wt::Dbo::Query<UIQueryResult> query = getUIQuery(session, filter);
|
||||
model.setQuery(query, columnNames.empty() ? true : false);
|
||||
|
||||
// TODO do something better
|
||||
if (columnNames.size() == 2)
|
||||
{
|
||||
model.addColumn( "c.name", columnNames[0] );
|
||||
model.addColumn( "COUNT(DISTINCT t.id)", columnNames[1] );
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
|
||||
{
|
||||
|
||||
+18
-30
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <Wt/Dbo/Dbo>
|
||||
#include <Wt/Dbo/WtSqlTraits>
|
||||
@@ -44,6 +45,12 @@ class Cluster
|
||||
{
|
||||
public:
|
||||
|
||||
enum class Type
|
||||
{
|
||||
Genre = 1,
|
||||
Mood = 2,
|
||||
};
|
||||
|
||||
typedef Wt::Dbo::ptr<Cluster> pointer;
|
||||
typedef Wt::Dbo::dbo_traits<Cluster>::IdType id_type;
|
||||
|
||||
@@ -57,12 +64,6 @@ class Cluster
|
||||
static Wt::Dbo::collection<pointer> getAll(Wt::Dbo::Session& session);
|
||||
static std::vector<pointer> getByType(Wt::Dbo::Session& session, std::string type);
|
||||
|
||||
// MVC models for the user interface
|
||||
// ClusterID, type, name, track count
|
||||
typedef boost::tuple<id_type, std::string, int> UIQueryResult;
|
||||
static Wt::Dbo::Query<UIQueryResult> getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
|
||||
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
|
||||
|
||||
// Create utility
|
||||
static pointer create(Wt::Dbo::Session& session, std::string type, std::string name);
|
||||
|
||||
@@ -118,8 +119,13 @@ class Track
|
||||
static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p);
|
||||
static pointer getById(Wt::Dbo::Session& session, id_type id);
|
||||
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool &moreResults);
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
|
||||
const std::vector<id_type>& clusters, // tracks that belong to these clusters
|
||||
const std::vector<std::string> keywords, // name must match all of these keywords
|
||||
int offset,
|
||||
int size,
|
||||
bool& moreExpected);
|
||||
|
||||
static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session);
|
||||
static std::vector<id_type> getAllIds(Wt::Dbo::Session& session); // nested transaction
|
||||
static std::vector<boost::filesystem::path> getAllPaths(Wt::Dbo::Session& session); // nested transaction
|
||||
@@ -127,22 +133,6 @@ class Track
|
||||
static std::vector<pointer> getChecksumDuplicates(Wt::Dbo::Session& session);
|
||||
|
||||
// Utility fonctions
|
||||
// MVC models for the user interface
|
||||
// ID, Artist name, Release Name, DiscNumber, TrackNumber, Name, duration, date, original date, genre list
|
||||
typedef boost::tuple<id_type, // ID
|
||||
std::string, // Artist name
|
||||
std::string, // Release Name
|
||||
int, // Disc Number
|
||||
int, // Track Number
|
||||
std::string, // Name
|
||||
boost::posix_time::time_duration, // Duration
|
||||
boost::posix_time::ptime, // Date
|
||||
boost::posix_time::ptime, // Original date
|
||||
std::string> // genre list
|
||||
UIQueryResult;
|
||||
static Wt::Dbo::Query< UIQueryResult > getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
|
||||
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
|
||||
|
||||
// Stats for a given search filter
|
||||
typedef boost::tuple<
|
||||
int, // Total tracks
|
||||
@@ -174,10 +164,10 @@ class Track
|
||||
void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; }
|
||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||
|
||||
int getTrackNumber(void) const { return _trackNumber; }
|
||||
int getTotalTrackNumber(void) const { return _totalTrackNumber; }
|
||||
int getDiscNumber(void) const { return _discNumber; }
|
||||
int getTotalDiscNumber(void) const { return _totalDiscNumber; }
|
||||
boost::optional<std::size_t> getTrackNumber(void) const;
|
||||
boost::optional<std::size_t> getTotalTrackNumber(void) const;
|
||||
boost::optional<std::size_t> getDiscNumber(void) const;
|
||||
boost::optional<std::size_t> getTotalDiscNumber(void) const;
|
||||
std::string getName(void) const { return _name; }
|
||||
boost::filesystem::path getPath(void) const { return _filePath; }
|
||||
boost::posix_time::time_duration getDuration(void) const { return _duration; }
|
||||
@@ -218,8 +208,6 @@ class Track
|
||||
|
||||
private:
|
||||
|
||||
static Wt::Dbo::Query< pointer > getQuery(Wt::Dbo::Session& session, SearchFilter filter);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
int _trackNumber;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
// header file aimed to ease database class declarations
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "DbArtist.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "Playlist.hpp"
|
||||
#include "Release.hpp"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WApplication>
|
||||
#include <Wt/WAnchor>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Artist::Artist(Filters* filters, Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent),
|
||||
_filters(filters)
|
||||
{
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
refresh();
|
||||
}));
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
Artist::refresh()
|
||||
{
|
||||
if (!wApp->internalPathMatches("/artist/"))
|
||||
return;
|
||||
|
||||
clear();
|
||||
|
||||
Database::Artist::id_type artistId;
|
||||
if (!readAs(wApp->internalPathNextPart("/artist/"), artistId))
|
||||
return;
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
auto artist = Database::Artist::getById(DboSession(), artistId);
|
||||
|
||||
if (!artist)
|
||||
{
|
||||
LmsApp->goHome();
|
||||
return;
|
||||
}
|
||||
|
||||
auto t = new Wt::WTemplate(Wt::WString::tr("template-artist"), this);
|
||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
t->bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::PlainText);
|
||||
|
||||
auto releasesContainer = new Wt::WContainerWidget();
|
||||
t->bindWidget("releases", releasesContainer);
|
||||
|
||||
auto releases = artist->getReleases(_filters->getClusterIds());
|
||||
for (auto release : releases)
|
||||
{
|
||||
if (release->isNone())
|
||||
continue;
|
||||
|
||||
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-artist-entry"), releasesContainer);
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
|
||||
{
|
||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(release.id())));
|
||||
Wt::WImage *cover = new Wt::WImage(coverAnchor);
|
||||
cover->setImageLink(SessionImageResource()->getReleaseUrl(release.id(), 128));
|
||||
// Some images may not be square
|
||||
cover->setWidth(128);
|
||||
entry->bindWidget("cover", coverAnchor);
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WAnchor *releaseAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(release.id())));
|
||||
Wt::WText *releaseName = new Wt::WText(releaseAnchor);
|
||||
releaseName->setText(Wt::WString::fromUTF8(release->getName(), Wt::PlainText));
|
||||
entry->bindWidget("name", releaseAnchor);
|
||||
}
|
||||
|
||||
if (release->hasVariousArtists())
|
||||
entry->setCondition("if-has-various-artists", true);
|
||||
|
||||
boost::optional<int> year = release->getReleaseYear();
|
||||
if (year)
|
||||
{
|
||||
entry->setCondition("if-has-year", true);
|
||||
entry->bindInt("year", *year);
|
||||
|
||||
boost::optional<int> originalYear = release->getReleaseYear(true);
|
||||
if (originalYear && *originalYear != *year)
|
||||
{
|
||||
entry->setCondition("if-has-orig-year", true);
|
||||
entry->bindInt("orig-year", *originalYear);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -20,19 +20,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "Filters.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class TrackSearchView : public Wt::WContainerWidget
|
||||
class Artist : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Artist(Filters* filters, Wt::WContainerWidget* parent = 0);
|
||||
|
||||
TrackSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent = 0);
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Filters* _filters;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WAnchor>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WLineEdit>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ArtistsView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
Artists::Artists(Filters* filters, Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent),
|
||||
_filters(filters)
|
||||
{
|
||||
auto artists = new Wt::WTemplate(Wt::WString::tr("template-artists"), this);
|
||||
artists->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
auto search = new Wt::WLineEdit();
|
||||
artists->bindWidget("search", search);
|
||||
search->setPlaceholderText(Wt::WString::tr("msg-search-placeholder"));
|
||||
search->textInput().connect(std::bind([this, search]
|
||||
{
|
||||
auto keywords = splitString(search->text().toUTF8(), " ");
|
||||
refresh(keywords);
|
||||
}));
|
||||
|
||||
_artistsContainer = new Wt::WContainerWidget();
|
||||
artists->bindWidget("artists", _artistsContainer);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Artists::refresh(std::vector<std::string> searchKeywords)
|
||||
{
|
||||
_artistsContainer->clear();
|
||||
|
||||
auto clusterIds = _filters->getClusterIds();
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
auto artists = Artist::getByFilter(DboSession(),
|
||||
clusterIds,
|
||||
searchKeywords,
|
||||
0, 40, moreResults);
|
||||
|
||||
for (auto artist : artists)
|
||||
{
|
||||
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-artists-entry"), _artistsContainer);
|
||||
|
||||
entry->bindInt("nb-release", artist->getReleases(clusterIds).size());
|
||||
|
||||
{
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(artist.id())));
|
||||
Wt::WText *artistText = new Wt::WText(artistAnchor);
|
||||
artistText->setText(Wt::WString::fromUTF8(artist->getName(), Wt::PlainText));
|
||||
entry->bindWidget("name", artistAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -21,27 +21,21 @@
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
#include "audio/Audio.hpp"
|
||||
|
||||
#include "MobilePlayQueueEvents.hpp"
|
||||
|
||||
#include "Filters.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class Audio : public UserInterface::Audio
|
||||
class Artists : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Audio(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void search(std::string text);
|
||||
Artists(Filters* filters, Wt::WContainerWidget* parent = 0);
|
||||
|
||||
private:
|
||||
void refresh(std::vector<std::string> searchKeywords = std::vector<std::string>());
|
||||
|
||||
PlayQueueEvents _playQueueEvents;
|
||||
|
||||
Filters* _filters;
|
||||
Wt::WContainerWidget* _artistsContainer;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WStackedWidget>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ArtistsView.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
#include "ReleasesView.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
#include "TracksView.hpp"
|
||||
|
||||
#include "Explore.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
|
||||
enum IdxRoot
|
||||
{
|
||||
IdxArtists = 0,
|
||||
IdxArtist,
|
||||
IdxReleases,
|
||||
IdxRelease,
|
||||
IdxTracks,
|
||||
};
|
||||
|
||||
static void
|
||||
handlePathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
static const std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/artists", IdxArtists },
|
||||
{ "/artist", IdxArtist },
|
||||
{ "/releases", IdxReleases },
|
||||
{ "/release", IdxRelease },
|
||||
{ "/tracks", IdxTracks },
|
||||
};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||
|
||||
for (auto index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
{
|
||||
stack->setCurrentIndex(index.second);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Explore::Explore(Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
auto container = new Wt::WTemplate(Wt::WString::tr("template-explore"), this);
|
||||
|
||||
auto filters = new Filters();
|
||||
container->bindWidget("filters", filters);
|
||||
|
||||
// Contents
|
||||
Wt::WStackedWidget* stack = new Wt::WStackedWidget();
|
||||
container->bindWidget("contents", stack);
|
||||
|
||||
stack->addWidget(new Artists(filters));
|
||||
stack->addWidget(new Artist(filters));
|
||||
stack->addWidget(new Releases(/*filters*/));
|
||||
stack->addWidget(new Release(/*filters*/));
|
||||
stack->addWidget(new Tracks(/*filters*/));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
handlePathChange(stack);
|
||||
}));
|
||||
|
||||
handlePathChange(stack);
|
||||
}
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -22,16 +22,12 @@
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class ArtistSearchView : public Wt::WContainerWidget
|
||||
class Explore : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
ArtistSearchView(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
Explore(Wt::WContainerWidget *parent = 0);
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,25 +17,29 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_AUDIO_HPP
|
||||
#define UI_AUDIO_HPP
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
#include <string>
|
||||
#include <Wt/WContainerWidget>
|
||||
#include "Filters.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Audio : public Wt::WContainerWidget
|
||||
Filters::Filters(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
public:
|
||||
auto container = new Wt::WTemplate(Wt::WString::tr("template-filters"), this);
|
||||
|
||||
Audio(Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent) {}
|
||||
virtual ~Audio() {}
|
||||
// Filters
|
||||
Wt::WPushButton *addFilterBtn = new Wt::WPushButton(Wt::WText::tr("msg-filter-add"));
|
||||
container->bindWidget("add-filter", addFilterBtn);
|
||||
|
||||
_filters = new Wt::WContainerWidget();
|
||||
container->bindWidget("filters", _filters);
|
||||
|
||||
// _filterIds = {108, 419};
|
||||
}
|
||||
|
||||
virtual void search(std::string text) = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -20,24 +20,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WLink>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
#include "Filters.hpp"
|
||||
|
||||
class ArtistView : public Wt::WContainerWidget
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Filters(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
static Wt::WLink getLink(Database::Artist::id_type id);
|
||||
std::vector<Database::Cluster::id_type> getClusterIds() const { return _filterIds; }
|
||||
|
||||
ArtistView(Wt::WContainerWidget *parent = 0);
|
||||
private:
|
||||
|
||||
Wt::WContainerWidget *_filters;
|
||||
std::vector<Database::Cluster::id_type> _filterIds;
|
||||
};
|
||||
|
||||
|
||||
} //namespace Mobile
|
||||
} //namespace UserInterface
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,20 +17,22 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Wt/WTemplate>
|
||||
|
||||
#include <Wt/WSignal>
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "HomeView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
struct PlayQueueEvents
|
||||
|
||||
Home::Home(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
Wt::Signal<Database::Track::id_type> trackPlay;
|
||||
Wt::Signal<Database::Track::id_type> trackAdd;
|
||||
};
|
||||
addWidget(new Wt::WTemplate(Wt::WString::tr("template-home")));
|
||||
}
|
||||
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -22,16 +22,18 @@
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class ReleaseSearchView : public Wt::WContainerWidget
|
||||
class Home : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Home(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
ReleaseSearchView(Wt::WContainerWidget* parent = 0);
|
||||
private:
|
||||
|
||||
void displayFilter(void);
|
||||
void displayContents(void);
|
||||
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
+111
-92
@@ -19,28 +19,19 @@
|
||||
|
||||
#include <Wt/WEnvironment>
|
||||
#include <Wt/WBootstrapTheme>
|
||||
#include <Wt/WVBoxLayout>
|
||||
#include <Wt/WNavigationBar>
|
||||
#include <Wt/WStackedWidget>
|
||||
#include <Wt/WMenu>
|
||||
#include <Wt/WPopupMenu>
|
||||
#include <Wt/WVBoxLayout>
|
||||
#include <Wt/Auth/Identity>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "settings/Settings.hpp"
|
||||
#include "settings/SettingsFirstConnectionFormView.hpp"
|
||||
|
||||
#include "auth/LmsAuth.hpp"
|
||||
#include "audio/desktop/DesktopAudio.hpp"
|
||||
#include "audio/mobile/MobileAudio.hpp"
|
||||
#if HAVE_VIDEO
|
||||
#include "video/VideoWidget.hpp"
|
||||
#endif
|
||||
#include "common/LineEdit.hpp"
|
||||
#include "Explore.hpp"
|
||||
#include "HomeView.hpp"
|
||||
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -48,19 +39,6 @@ namespace skeletons {
|
||||
extern const char *AuthStrings_xml1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool agentIsMobile()
|
||||
{
|
||||
const Wt::WEnvironment& env = Wt::WApplication::instance()->environment();
|
||||
return (env.agentIsIEMobile()
|
||||
|| env.agentIsMobileWebKit()
|
||||
|| env.userAgent().find("Mobile") != std::string::npos // Workaround for firefox
|
||||
|| env.userAgent().find("Tablet") != std::string::npos // Workaround for firefox
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Wt::WApplication*
|
||||
@@ -100,7 +78,13 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
|
||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||
|
||||
// Add a resource bundle
|
||||
messageResourceBundle().use(appRoot() + "artist");
|
||||
messageResourceBundle().use(appRoot() + "artists");
|
||||
messageResourceBundle().use(appRoot() + "messages");
|
||||
messageResourceBundle().use(appRoot() + "templates");
|
||||
messageResourceBundle().use(appRoot() + "release");
|
||||
messageResourceBundle().use(appRoot() + "releases");
|
||||
messageResourceBundle().use(appRoot() + "tracks");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
@@ -114,9 +98,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
|
||||
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << std::boolalpha << firstConnection;
|
||||
|
||||
// If here is no account in the database, launch the first connection wizard
|
||||
if (firstConnection)
|
||||
createFirstConnectionUI();
|
||||
else
|
||||
// if (firstConnection)
|
||||
// createFirstConnectionUI();
|
||||
// else
|
||||
createLmsUI();
|
||||
}
|
||||
|
||||
@@ -149,13 +133,19 @@ TranscodeResource* SessionTranscodeResource()
|
||||
return LmsApplication::instance()->getTranscodeResource();
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::goHome()
|
||||
{
|
||||
setInternalPath("/home", true);
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::createFirstConnectionUI()
|
||||
{
|
||||
// Hack, use the auth widget builtin strings
|
||||
builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1);
|
||||
|
||||
root()->addWidget( new Settings::FirstConnectionFormView());
|
||||
// root()->addWidget( new Settings::FirstConnectionFormView());
|
||||
}
|
||||
|
||||
void
|
||||
@@ -164,6 +154,8 @@ LmsApplication::createLmsUI()
|
||||
_imageResource = new ImageResource(_db, root());
|
||||
_transcodeResource = new TranscodeResource(_db, root());
|
||||
|
||||
handleAuthEvent();
|
||||
/*
|
||||
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
|
||||
|
||||
LmsAuth *authWidget = new LmsAuth();
|
||||
@@ -174,11 +166,53 @@ LmsApplication::createLmsUI()
|
||||
authWidget->processEnvironment();
|
||||
|
||||
root()->addWidget(authWidget);
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
enum IdxRoot
|
||||
{
|
||||
IdxHome = 0,
|
||||
IdxExplore,
|
||||
IdxPlaylist,
|
||||
IdxSettings,
|
||||
};
|
||||
|
||||
|
||||
|
||||
static void
|
||||
handlePathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
static const std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/home", IdxHome },
|
||||
{ "/artists", IdxExplore },
|
||||
{ "/artist", IdxExplore },
|
||||
{ "/releases", IdxExplore },
|
||||
{ "/release", IdxExplore },
|
||||
{ "/tracks", IdxExplore },
|
||||
{ "/playlist", IdxPlaylist },
|
||||
{ "/settings", IdxSettings },
|
||||
};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||
|
||||
for (auto index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
{
|
||||
stack->setCurrentIndex(index.second);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
wApp->setInternalPath("/home", true);
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleAuthEvent(void)
|
||||
{
|
||||
/*
|
||||
if (!DbHandler().getLogin().loggedIn())
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "User logged out, session = " << Wt::WApplication::instance()->sessionId();
|
||||
@@ -190,76 +224,61 @@ LmsApplication::handleAuthEvent(void)
|
||||
}
|
||||
|
||||
LMS_LOG(UI, INFO) << "User '" << CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().agent() << ", session = " << Wt::WApplication::instance()->sessionId();
|
||||
*/
|
||||
setConfirmCloseMessage(Wt::WString::tr("msg-quit-confirm"));
|
||||
|
||||
this->root()->setOverflow(Wt::WContainerWidget::OverflowHidden);
|
||||
setConfirmCloseMessage("Closing LMS. Are you sure?");
|
||||
auto main = new Wt::WTemplate(Wt::WString::tr("template-main"), root());
|
||||
|
||||
// Create a Vertical layout: top is the nav bar, bottom is the contents
|
||||
Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root());
|
||||
// Navbar
|
||||
auto navbar = new Wt::WNavigationBar();
|
||||
navbar->setTitle("LMS", Wt::WLink(Wt::WLink::InternalPath, "/home"));
|
||||
navbar->setResponsive(true);
|
||||
// navbar->setStyleClass("main-nav");
|
||||
|
||||
Wt::WNavigationBar *navigation = new Wt::WNavigationBar();
|
||||
navigation->setTitle("LMS", "https://github.com/epoupon/lms");
|
||||
navigation->setResponsive(true);
|
||||
navigation->addStyleClass("main-nav");
|
||||
main->bindWidget("navbar-top", navbar);
|
||||
|
||||
Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget();
|
||||
|
||||
contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto);
|
||||
contentsStack->addStyleClass("contents");
|
||||
|
||||
Wt::WMenu *menu = new Wt::WMenu(contentsStack);
|
||||
navigation->addMenu(menu, Wt::AlignRight);
|
||||
|
||||
LineEdit *searchEdit = new LineEdit(500);
|
||||
navigation->bindWidget("search", searchEdit);
|
||||
searchEdit->setEmptyText("Search...");
|
||||
searchEdit->addStyleClass("navbar-form navbar-nav");
|
||||
searchEdit->setWidth(150);
|
||||
// TODO add a span with a search icon
|
||||
|
||||
menu->setInternalPathEnabled();
|
||||
menu->setInternalBasePath("/");
|
||||
|
||||
Audio *audio;
|
||||
if (agentIsMobile())
|
||||
audio = new Mobile::Audio();
|
||||
else
|
||||
audio = new Desktop::Audio();
|
||||
|
||||
menu->addItem("Audio", audio)->setPathComponent("audio");;
|
||||
#if defined HAVE_VIDEO
|
||||
menu->addItem("Video", new VideoWidget())->setPathComponent("video");
|
||||
#endif
|
||||
menu->addItem("Settings", new Settings::Settings())->setPathComponent("settings");
|
||||
|
||||
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
|
||||
popup->addItem("Logout");
|
||||
|
||||
Wt::WMenuItem *item = new Wt::WMenuItem( CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) );
|
||||
item->setMenu(popup);
|
||||
menu->addItem(item);
|
||||
|
||||
popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item)
|
||||
auto menu = new Wt::WMenu();
|
||||
{
|
||||
if (item && item->text() == "Logout")
|
||||
DbHandler().getLogin().logout();
|
||||
}, std::placeholders::_1));
|
||||
|
||||
searchEdit->timedChanged().connect(std::bind([=] ()
|
||||
auto menuItem = menu->insertItem(0, Wt::WString::tr("msg-artists"));
|
||||
menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/artists"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
// TODO: check which view is activated and search into it
|
||||
audio->search(searchEdit->text().toUTF8());
|
||||
auto menuItem = menu->insertItem(1, Wt::WString::tr("msg-releases"));
|
||||
menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/releases"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(2, Wt::WString::tr("msg-tracks"));
|
||||
menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/tracks"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(3, Wt::WString::tr("msg-playlist"));
|
||||
menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/playlist"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
navbar->addMenu(menu);
|
||||
|
||||
// Contents
|
||||
Wt::WStackedWidget* mainStack = new Wt::WStackedWidget();
|
||||
main->bindWidget("contents", mainStack);
|
||||
|
||||
mainStack->addWidget(new Home());
|
||||
mainStack->addWidget(new Explore());
|
||||
mainStack->addWidget(new Wt::WText("PLAYLIST"));
|
||||
mainStack->addWidget(new Wt::WText("SETTINGS"));
|
||||
|
||||
// MediaPlayer
|
||||
// auto player = new AudioPlayer(AudioPlayer::ControlPlayqueue);
|
||||
// main->bindWidget("player", player);
|
||||
|
||||
internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
handlePathChange(mainStack);
|
||||
}));
|
||||
|
||||
layout->addWidget(navigation);
|
||||
layout->addWidget(contentsStack, 1);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
if (agentIsMobile())
|
||||
setInternalPath("/audio/search/preview", true);
|
||||
else
|
||||
setInternalPath("/audio");
|
||||
|
||||
handlePathChange(mainStack);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -44,6 +44,9 @@ class LmsApplication : public Wt::WApplication
|
||||
TranscodeResource* getTranscodeResource() { return _transcodeResource; }
|
||||
Database::Handler& getDbHandler() { return _db;}
|
||||
|
||||
// Utils
|
||||
void goHome();
|
||||
|
||||
private:
|
||||
|
||||
void handleAuthEvent(void);
|
||||
@@ -67,6 +70,7 @@ Database::User::pointer CurrentUser();
|
||||
ImageResource *SessionImageResource();
|
||||
TranscodeResource *SessionTranscodeResource();
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WApplication>
|
||||
#include <Wt/WAnchor>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Release::Release(Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
refresh();
|
||||
}));
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
Release::refresh()
|
||||
{
|
||||
if (!wApp->internalPathMatches("/release/"))
|
||||
return;
|
||||
|
||||
clear();
|
||||
|
||||
Database::Release::id_type releaseId;
|
||||
|
||||
if (!readAs(wApp->internalPathNextPart("/release/"), releaseId))
|
||||
return;
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
auto release = Database::Release::getById(DboSession(), releaseId);
|
||||
if (!release)
|
||||
{
|
||||
LmsApp->goHome();
|
||||
return;
|
||||
}
|
||||
|
||||
auto t = new Wt::WTemplate(Wt::WString::tr("template-release"), this);
|
||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
||||
|
||||
boost::optional<int> year = release->getReleaseYear();
|
||||
if (year)
|
||||
{
|
||||
t->setCondition("if-has-year", true);
|
||||
t->bindInt("year", *year);
|
||||
|
||||
boost::optional<int> originalYear = release->getReleaseYear(true);
|
||||
if (originalYear && *originalYear != *year)
|
||||
{
|
||||
t->setCondition("if-has-orig-year", true);
|
||||
t->bindInt("orig-year", *originalYear);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto artists = release->getArtists();
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
t->bindString("artist-name", Wt::WString::tr("msg-various-artists"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(artists.front().id())));
|
||||
Wt::WText *artist = new Wt::WText(artistAnchor);
|
||||
artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText));
|
||||
t->bindWidget("artist-name", artistAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
Wt::WImage *cover = new Wt::WImage();
|
||||
cover->setImageLink(Wt::WLink (SessionImageResource()->getReleaseUrl(release.id(), 512)));
|
||||
t->bindWidget("cover", cover);
|
||||
|
||||
// TODO play/add button
|
||||
|
||||
auto tracksContainer = new Wt::WContainerWidget();
|
||||
t->bindWidget("tracks", tracksContainer);
|
||||
|
||||
std::vector<Database::Cluster::id_type> clusterIds; // TODO fill
|
||||
|
||||
// auto tracks = release->getTracks(clusterIds);
|
||||
auto tracks = release->getTracks();
|
||||
bool variousArtists = release->hasVariousArtists();
|
||||
|
||||
for (auto track : tracks)
|
||||
{
|
||||
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-release-entry"), tracksContainer);
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||
|
||||
if (variousArtists && !track->getArtist()->isNone())
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(track->getArtist().id())));
|
||||
Wt::WText *artistText = new Wt::WText(artistAnchor);
|
||||
artistText->setText(Wt::WString::fromUTF8(track->getArtist()->getName(), Wt::PlainText));
|
||||
entry->bindWidget("artist-name", artistAnchor);
|
||||
}
|
||||
|
||||
auto trackNumber = track->getTrackNumber();
|
||||
if (trackNumber)
|
||||
{
|
||||
entry->setCondition("if-has-track-number", true);
|
||||
entry->bindInt("track-number", *trackNumber);
|
||||
}
|
||||
|
||||
auto discNumber = track->getDiscNumber();
|
||||
auto totalDiscNumber = track->getTotalDiscNumber();
|
||||
if (discNumber && totalDiscNumber && *totalDiscNumber > 1)
|
||||
{
|
||||
entry->setCondition("if-has-disc-number", true);
|
||||
entry->bindInt("disc-number", *discNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -17,18 +17,20 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class Settings : public Wt::WContainerWidget
|
||||
class Release : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Settings(Wt::WContainerWidget* parent = 0);
|
||||
Release(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WApplication>
|
||||
#include <Wt/WAnchor>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WTemplate>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "ReleasesView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
Releases::Releases(Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
auto releases = new Wt::WTemplate(Wt::WString::tr("template-releases"), this);
|
||||
releases->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
auto search = new Wt::WLineEdit();
|
||||
releases->bindWidget("search", search);
|
||||
search->setPlaceholderText(Wt::WString::tr("msg-search-placeholder"));
|
||||
search->textInput().connect(std::bind([this, search]
|
||||
{
|
||||
auto keywords = splitString(search->text().toUTF8(), " ");
|
||||
refresh(keywords);
|
||||
}));
|
||||
|
||||
_releasesContainer = new Wt::WContainerWidget();
|
||||
releases->bindWidget("releases", _releasesContainer);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Releases::refresh(std::vector<std::string> searchKeywords)
|
||||
{
|
||||
_releasesContainer->clear();
|
||||
|
||||
std::vector<Cluster::id_type> clusterIds; // TODO fill
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
auto releases = Release::getByFilter(DboSession(), clusterIds, searchKeywords, 0, 40, moreResults);
|
||||
|
||||
for (auto release : releases)
|
||||
{
|
||||
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-releases-entry"), _releasesContainer);
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(release.id())));
|
||||
Wt::WImage *cover = new Wt::WImage(coverAnchor);
|
||||
cover->setImageLink(SessionImageResource()->getReleaseUrl(release.id(), 128));
|
||||
// Some images may not be square
|
||||
cover->setWidth(128);
|
||||
entry->bindWidget("cover", coverAnchor);
|
||||
|
||||
entry->bindString("release-name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
||||
|
||||
auto artists = release->getArtists();
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->bindString("artist-name", Wt::WString::tr("msg-various-artists"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(artists.front().id())));
|
||||
Wt::WText *artist = new Wt::WText(artistAnchor);
|
||||
artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText));
|
||||
entry->bindWidget("artist-name", artistAnchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Releases : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Releases(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
private:
|
||||
void refresh(std::vector<std::string> searchKeywords = std::vector<std::string>());
|
||||
|
||||
Wt::WContainerWidget *_releasesContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/WAnchor>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "TracksView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
Tracks::Tracks(Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
auto tracks = new Wt::WTemplate(Wt::WString::tr("template-tracks"), this);
|
||||
tracks->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
auto search = new Wt::WLineEdit();
|
||||
tracks->bindWidget("search", search);
|
||||
search->setPlaceholderText(Wt::WString::tr("msg-search-placeholder"));
|
||||
search->textInput().connect(std::bind([this, search]
|
||||
{
|
||||
auto keywords = splitString(search->text().toUTF8(), " ");
|
||||
refresh(keywords);
|
||||
}));
|
||||
|
||||
_tracksContainer = new Wt::WContainerWidget();
|
||||
tracks->bindWidget("tracks", _tracksContainer);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Tracks::refresh(std::vector<std::string> searchKeywords)
|
||||
{
|
||||
_tracksContainer->clear();
|
||||
|
||||
std::vector<Cluster::id_type> clusterIds; // TODO fill
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
auto tracks = Track::getByFilter(DboSession(), clusterIds, searchKeywords, 0, 40, moreResults);
|
||||
|
||||
for (auto track : tracks)
|
||||
{
|
||||
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-tracks-entry"), _tracksContainer);
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||
|
||||
auto artist = track->getArtist();
|
||||
if (!artist->isNone())
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(track->getArtist().id())));
|
||||
Wt::WText *artistText = new Wt::WText(artistAnchor);
|
||||
artistText->setText(Wt::WString::fromUTF8(artist->getName(), Wt::PlainText));
|
||||
entry->bindWidget("artist-name", artistAnchor);
|
||||
}
|
||||
|
||||
auto release = track->getRelease();
|
||||
if (!release->isNone())
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
// TODO anchor
|
||||
Wt::WAnchor *releaseAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(track->getRelease().id())));
|
||||
Wt::WText *releaseText = new Wt::WText(releaseAnchor);
|
||||
releaseText->setText(Wt::WString::fromUTF8(release->getName(), Wt::PlainText));
|
||||
entry->bindWidget("release-name", releaseAnchor);
|
||||
}
|
||||
|
||||
Wt::WImage *cover = new Wt::WImage();
|
||||
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
|
||||
// Some images may not be square
|
||||
cover->setWidth(64);
|
||||
entry->bindWidget("cover", cover);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -21,19 +21,18 @@
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
#include "MobilePlayQueueEvents.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class PreviewSearchView : public Wt::WContainerWidget
|
||||
class Tracks : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Tracks(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
PreviewSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent = 0);
|
||||
private:
|
||||
void refresh(std::vector<std::string> searchKeywords = std::vector<std::string>());
|
||||
|
||||
Wt::WContainerWidget *_tracksContainer;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WText>
|
||||
#include <Wt/WAudio>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
#include "common/InputRange.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "AudioPlayer.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
|
||||
Av::Encoding
|
||||
AudioPlayer::getBestEncoding() const
|
||||
{
|
||||
// TODO get the supported formats of the player and pick one
|
||||
return Av::Encoding::MP3;
|
||||
}
|
||||
|
||||
bool
|
||||
AudioPlayer::loadTrack(Database::Track::id_type trackId)
|
||||
{
|
||||
// TODO reduce this scope
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
||||
if (!track)
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "No track found for id " << trackId;
|
||||
return false;
|
||||
}
|
||||
|
||||
Database::User::pointer user = CurrentUser();
|
||||
if (!user)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "No user found!";
|
||||
return false;
|
||||
}
|
||||
|
||||
Av::Encoding encoding;
|
||||
switch (user->getAudioEncoding())
|
||||
{
|
||||
case Database::AudioEncoding::MP3: encoding = Av::Encoding::MP3; break;
|
||||
case Database::AudioEncoding::OGA: encoding = Av::Encoding::OGA; break;
|
||||
case Database::AudioEncoding::WEBMA: encoding = Av::Encoding::WEBMA; break;
|
||||
case Database::AudioEncoding::AUTO:
|
||||
default:
|
||||
encoding = getBestEncoding();
|
||||
break;
|
||||
}
|
||||
|
||||
bindString("track", Wt::WString::fromUTF8(track->getName()));
|
||||
bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName()));
|
||||
_cover->setImageLink(SessionImageResource()->getTrackUrl(trackId, 64));
|
||||
|
||||
std::string durationFormat = track->getDuration().total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
||||
|
||||
_trackDuration->setText(durationToString(track->getDuration(), durationFormat));
|
||||
_trackCurTime->setText(durationToString(boost::posix_time::seconds(0), durationFormat));
|
||||
|
||||
// Analyse track, select the best media stream
|
||||
Av::MediaFile mediaFile(track->getPath());
|
||||
|
||||
if (!mediaFile.open() || !mediaFile.scan())
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
|
||||
std::vector<std::size_t> streams;
|
||||
if (audioBestStreamId != -1)
|
||||
streams.push_back(audioBestStreamId);
|
||||
|
||||
this->doJavaScript("\
|
||||
document.lms.audio.state = \"loaded\";\
|
||||
document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
|
||||
document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
|
||||
document.lms.audio.seekbar.value = 0;\
|
||||
document.lms.audio.seekbar.disabled = false;\
|
||||
document.lms.audio.offset = 0;\
|
||||
document.lms.audio.curTime = 0;\
|
||||
");
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Loading, URL = '" << SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams) << "'";
|
||||
|
||||
//TODO, try to load everything in JS in order to prevent the WriteError bug?
|
||||
_audio->pause();
|
||||
_audio->clearSources();
|
||||
_audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams));
|
||||
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
||||
_audio->play();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
AudioPlayer::AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplate(parent)
|
||||
{
|
||||
setTemplateText(Wt::WString::tr("wa-audio-player"));
|
||||
addStyleClass("mediaplayer");
|
||||
|
||||
// TODO potential leak here
|
||||
_audio = new Wt::WAudio();
|
||||
_audio->setOptions(Wt::WAudio::Autoplay);
|
||||
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
||||
bindWidget("audio", _audio);
|
||||
|
||||
_audio->ended().connect(std::bind([=] ()
|
||||
{
|
||||
_playbackEnded.emit();
|
||||
}));
|
||||
|
||||
_cover = new Wt::WImage();
|
||||
bindWidget("cover", _cover);
|
||||
_cover->setImageLink(SessionImageResource()->getUnknownTrackUrl(64));
|
||||
|
||||
InputRange *seekbar = new InputRange();
|
||||
seekbar->addStyleClass("mediaplayer-seekbar");
|
||||
bindWidget("seekbar", seekbar);
|
||||
|
||||
bindString("track", "Track");
|
||||
bindString("artist", "Artist");
|
||||
|
||||
InputRange *volumeSlider = new InputRange();
|
||||
volumeSlider->addStyleClass("mediaplayer-volume");
|
||||
volumeSlider->setAttributeValue("orient", "vertical"); // firefox
|
||||
bindWidget("volume", volumeSlider);
|
||||
|
||||
if (controls & ControlRepeat)
|
||||
{
|
||||
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([=] ()
|
||||
{
|
||||
if (repeatBtn->hasStyleClass("mediaplayer-btn-active"))
|
||||
{
|
||||
repeatBtn->removeStyleClass("mediaplayer-btn-active");
|
||||
_loop.emit(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
repeatBtn->addStyleClass("mediaplayer-btn-active");
|
||||
_loop.emit(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (controls & ControlPlayqueue)
|
||||
{
|
||||
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([=] ()
|
||||
{
|
||||
_playQueue.emit();
|
||||
}));
|
||||
}
|
||||
|
||||
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([=] ()
|
||||
{
|
||||
if (shuffleBtn->hasStyleClass("mediaplayer-btn-active"))
|
||||
{
|
||||
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);
|
||||
prevBtn->addStyleClass("mediaplayer-btn");
|
||||
bindWidget("prev", prevBtn);
|
||||
prevBtn->clicked().connect(std::bind([=] ()
|
||||
{
|
||||
_playPrevious.emit();
|
||||
}));
|
||||
|
||||
Wt::WText *nextBtn = new Wt::WText("<i class=\"fa fa-step-forward fa-2x\"></i>", Wt::XHTMLText);
|
||||
nextBtn->addStyleClass("mediaplayer-btn");
|
||||
bindWidget("next", nextBtn);
|
||||
nextBtn->clicked().connect(std::bind([=] ()
|
||||
{
|
||||
_playNext.emit();
|
||||
}));
|
||||
|
||||
Wt::WText *playPauseBtn = new Wt::WText("<i class=\"fa fa-play fa-3x fa-fw\"></i>", Wt::XHTMLText);
|
||||
playPauseBtn->addStyleClass("mediaplayer-btn");
|
||||
bindWidget("play-pause", playPauseBtn);
|
||||
|
||||
_trackCurTime = new Wt::WText("--:--");
|
||||
_trackCurTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration");
|
||||
bindWidget("curtime", _trackCurTime);
|
||||
|
||||
_trackDuration = new Wt::WText("--:--");
|
||||
_trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration");
|
||||
bindWidget("duration", _trackDuration);
|
||||
|
||||
this->doJavaScript(
|
||||
"\
|
||||
document.lms = {};\
|
||||
document.lms.audio = {};\
|
||||
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
||||
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
||||
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
||||
document.lms.audio.curTimeText = " + _trackCurTime->jsRef() + ";\
|
||||
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
||||
\
|
||||
document.lms.audio.offset = 0;\
|
||||
document.lms.audio.curTime = 0;\
|
||||
document.lms.audio.state = \"init\";\
|
||||
document.lms.audio.volume = 1;\
|
||||
\
|
||||
document.lms.audio.seekbar.value = 0;\
|
||||
document.lms.audio.seekbar.disabled = true;\
|
||||
\
|
||||
document.lms.audio.volumeSlider.min = 0;\
|
||||
document.lms.audio.volumeSlider.max = 100;\
|
||||
document.lms.audio.volumeSlider.value = 100;\
|
||||
\
|
||||
function updateUIPlaying() { \
|
||||
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
||||
icon.className = \"fa fa-pause fa-3x fa-fw\"; \
|
||||
} \
|
||||
\
|
||||
function updateUIStopped() { \
|
||||
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
||||
icon.className = \"fa fa-play fa-3x fa-fw\"; \
|
||||
} \
|
||||
\
|
||||
function durationToString(duration, displayHours) { \
|
||||
var hours = parseInt( duration / 3600 ) % 24; \
|
||||
var minutes = parseInt( duration / 60) % 60; \
|
||||
var seconds = duration % 60; \
|
||||
\
|
||||
var res = \"\"; \
|
||||
if (displayHours) \
|
||||
res = (hours < 10 ? \"0\" + hours : hours) + \":\"; \
|
||||
\
|
||||
res += (minutes < 10 ? \"0\" + minutes : minutes) + \":\"; \
|
||||
res += (seconds < 10 ? \"0\" + seconds : seconds); \
|
||||
return res; \
|
||||
} \
|
||||
\
|
||||
function updateUI() {\
|
||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
||||
document.lms.audio.seekbar.value = document.lms.audio.curTime;\
|
||||
}\
|
||||
\
|
||||
var mouseDown = 0;\
|
||||
function seekMouseDown(e) {\
|
||||
++mouseDown;\
|
||||
}\
|
||||
function seekMouseUp(e) {\
|
||||
--mouseDown;\
|
||||
}\
|
||||
\
|
||||
function seeking(e) {\
|
||||
if (document.lms.audio.state == \"init\")\
|
||||
return;\
|
||||
\
|
||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.seekbar.value, document.lms.audio.seekbar.max > 3600); \
|
||||
}\
|
||||
\
|
||||
function seek(e) {\
|
||||
if (document.lms.audio.state == \"init\")\
|
||||
return;\
|
||||
\
|
||||
document.lms.audio.audio.pause(); \
|
||||
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
||||
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
||||
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
||||
var src = audioSource.src;\
|
||||
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
||||
audioSource.src = src + document.lms.audio.seekbar.value;\
|
||||
document.lms.audio.audio.load(); \
|
||||
document.lms.audio.audio.play(); \
|
||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
||||
}\
|
||||
\
|
||||
function volumeChanged() {\
|
||||
document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
|
||||
}\
|
||||
\
|
||||
function updateCurTime() {\
|
||||
document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
|
||||
if (mouseDown == 0)\
|
||||
updateUI();\
|
||||
} \
|
||||
\
|
||||
function playPause() { \
|
||||
if (document.lms.audio.state == \"init\") \
|
||||
return;\
|
||||
\
|
||||
if (document.lms.audio.audio.paused) \
|
||||
document.lms.audio.audio.play(); \
|
||||
else \
|
||||
document.lms.audio.audio.pause();\
|
||||
\
|
||||
}\
|
||||
\
|
||||
document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
|
||||
document.lms.audio.audio.addEventListener('playing', updateUIPlaying); \
|
||||
document.lms.audio.audio.addEventListener('play', updateUIPlaying); \
|
||||
document.lms.audio.audio.addEventListener('pause', updateUIStopped); \
|
||||
document.lms.audio.audio.addEventListener('ended', updateUIStopped); \
|
||||
document.lms.audio.seekbar.addEventListener('change', seek);\
|
||||
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
||||
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
||||
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
||||
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
||||
document.lms.audio.playPause.addEventListener('click', playPause);\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
} // namespaceUserInterface
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <Wt/WTemplate>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "common/InputRange.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioPlayer : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
|
||||
enum ControlFlags
|
||||
{
|
||||
ControlNone = 0,
|
||||
ControlShuffle = 1 << 0,
|
||||
ControlRepeat = 1 << 1,
|
||||
ControlPlayqueue = 1 << 2,
|
||||
};
|
||||
|
||||
AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
Av::Encoding getBestEncoding() const;
|
||||
bool loadTrack(Database::Track::id_type trackId);
|
||||
|
||||
// Signals
|
||||
Wt::Signal<void>& playbackEnded() {return _playbackEnded;}
|
||||
Wt::Signal<void>& playNext() {return _playNext;}
|
||||
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
||||
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
||||
Wt::Signal<bool>& loop() {return _loop;}
|
||||
Wt::Signal<void>& showPlayQueue() {return _playQueue;}
|
||||
|
||||
private:
|
||||
|
||||
// Signals
|
||||
Wt::Signal<void> _playbackEnded;
|
||||
Wt::Signal<void> _playNext;
|
||||
Wt::Signal<void> _playPrevious;
|
||||
Wt::Signal<bool> _shuffle;
|
||||
Wt::Signal<bool> _loop;
|
||||
Wt::Signal<void> _playQueue;
|
||||
|
||||
Wt::WAudio* _audio;
|
||||
Wt::WText* _trackCurTime;
|
||||
Wt::WText* _trackDuration;
|
||||
Wt::WText* _trackName;
|
||||
Wt::WImage* _cover;
|
||||
};
|
||||
|
||||
constexpr AudioPlayer::ControlFlags operator|(AudioPlayer::ControlFlags f1, AudioPlayer::ControlFlags f2) { return AudioPlayer::ControlFlags(int(f1)|int(f2)); }
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -1,556 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
#include <Wt/WVBoxLayout>
|
||||
#include <Wt/WHBoxLayout>
|
||||
#include <Wt/WGridLayout>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WMessageBox>
|
||||
|
||||
#include <Wt/WLengthValidator>
|
||||
#include <Wt/WDialog>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WLabel>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "TableFilter.hpp"
|
||||
#include "KeywordSearchFilter.hpp"
|
||||
|
||||
#include "DesktopAudio.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void WPopupMenuClear(Wt::WPopupMenu* menu)
|
||||
{
|
||||
while(menu->count() > 0)
|
||||
menu->removeItem(menu->itemAt(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
// Special playlist generated each time the playque gets changed
|
||||
// Restored at the beginning of the session
|
||||
static const std::string CurrentQueuePlaylistName = "__current__";
|
||||
|
||||
Audio::Audio(Wt::WContainerWidget* parent)
|
||||
: UserInterface::Audio(parent),
|
||||
_mediaPlayer(nullptr),
|
||||
_trackView(nullptr),
|
||||
_playQueue(nullptr)
|
||||
{
|
||||
Wt::WGridLayout *mainLayout = new Wt::WGridLayout();
|
||||
this->setLayout(mainLayout);
|
||||
mainLayout->setContentsMargins(9,4,9,9);
|
||||
|
||||
// Filters
|
||||
Wt::WHBoxLayout *filterLayout = new Wt::WHBoxLayout();
|
||||
|
||||
TableFilterCluster *filterCluster = new TableFilterCluster();
|
||||
filterLayout->addWidget(filterCluster);
|
||||
_filterChain.addFilter(filterCluster);
|
||||
|
||||
TableFilterArtist *filterArtist = new TableFilterArtist();
|
||||
filterLayout->addWidget(filterArtist);
|
||||
_filterChain.addFilter(filterArtist);
|
||||
|
||||
TableFilterRelease *filterRelease = new TableFilterRelease();
|
||||
filterLayout->addWidget(filterRelease);
|
||||
_filterChain.addFilter(filterRelease);
|
||||
|
||||
mainLayout->addLayout(filterLayout, 0, 1);
|
||||
// ENDOF(Filters)
|
||||
|
||||
// TODO ADD some stats (nb files, total duration, etc.)
|
||||
|
||||
Wt::WVBoxLayout* trackLayout = new Wt::WVBoxLayout();
|
||||
|
||||
_trackView = new TrackView();
|
||||
trackLayout->addWidget(_trackView, 1);
|
||||
|
||||
Wt::WHBoxLayout* trackControls = new Wt::WHBoxLayout();
|
||||
|
||||
Wt::WPushButton* playBtn = new Wt::WPushButton("Play");
|
||||
playBtn->setStyleClass("btn-sm");
|
||||
trackControls->addWidget(playBtn);
|
||||
|
||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add");
|
||||
addBtn->setStyleClass("btn-sm");
|
||||
trackControls->addWidget(addBtn);
|
||||
|
||||
Wt::WText *statsText = new Wt::WText();
|
||||
statsText->setStyleClass("vertical-align");
|
||||
trackControls->addWidget(statsText, 1);
|
||||
_trackView->statsUpdated().connect(std::bind([=] (Wt::WString stats) {
|
||||
statsText->setText(stats);
|
||||
}, std::placeholders::_1));
|
||||
|
||||
trackLayout->addLayout(trackControls);
|
||||
|
||||
mainLayout->addLayout(trackLayout, 1, 1);
|
||||
|
||||
_filterChain.addFilter(_trackView);
|
||||
|
||||
|
||||
_playQueue = new PlayQueue();
|
||||
|
||||
// Playlist/PlayQueue
|
||||
Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
|
||||
playQueueContainer->setStyleClass("playqueue");
|
||||
Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
|
||||
playQueueContainer->setLayout(playQueueLayout);
|
||||
|
||||
playQueueLayout->addWidget( _playQueue, 1);
|
||||
|
||||
Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
|
||||
|
||||
Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
|
||||
playlistBtn->setStyleClass("btn-sm btn-primary");
|
||||
playlistControls->addWidget(playlistBtn);
|
||||
|
||||
// Playlist menu
|
||||
{
|
||||
Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
|
||||
|
||||
_popupMenuSave = new Wt::WPopupMenu();
|
||||
popupMain->addMenu("Save", _popupMenuSave);
|
||||
|
||||
_popupMenuLoad = new Wt::WPopupMenu();
|
||||
popupMain->addMenu("Load", _popupMenuLoad);
|
||||
|
||||
_popupMenuDelete = new Wt::WPopupMenu();
|
||||
popupMain->addMenu("Delete", _popupMenuDelete);
|
||||
|
||||
playlistBtn->setMenu(popupMain);
|
||||
}
|
||||
|
||||
#if WT_VERSION >= 0X03030400
|
||||
Wt::WPushButton *upBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-up fa-lg\"></i>", Wt::XHTMLText);
|
||||
#else
|
||||
Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
|
||||
#endif
|
||||
upBtn->setStyleClass("btn-sm");
|
||||
playlistControls->addWidget(upBtn);
|
||||
|
||||
#if WT_VERSION >= 0X03030400
|
||||
Wt::WPushButton *downBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-down fa-lg\"></i>", Wt::XHTMLText);
|
||||
#else
|
||||
Wt::WPushButton *downBtn = new Wt::WPushButton("DOWN");
|
||||
#endif
|
||||
downBtn->setStyleClass("btn-sm");
|
||||
playlistControls->addWidget(downBtn);
|
||||
#if WT_VERSION >= 0X03030400
|
||||
Wt::WPushButton *delBtn = new Wt::WPushButton("<i class =\"fa fa-remove fa-lg\"></i>", Wt::XHTMLText);
|
||||
#else
|
||||
Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
|
||||
#endif
|
||||
|
||||
delBtn->setStyleClass("btn-sm btn-warning");
|
||||
playlistControls->addWidget(delBtn);
|
||||
#if WT_VERSION >= 0X03030400
|
||||
Wt::WPushButton *clearBtn = new Wt::WPushButton("<i class =\"fa fa-trash fa-lg\"></i>", Wt::XHTMLText);
|
||||
#else
|
||||
Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
|
||||
#endif
|
||||
clearBtn->setStyleClass("btn-sm btn-danger");
|
||||
playlistControls->addWidget(clearBtn);
|
||||
|
||||
delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
|
||||
upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
|
||||
downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
|
||||
clearBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
||||
("Clear playqueue",
|
||||
Wt::WString( "Are you sure?"),
|
||||
Wt::Question, Wt::Yes | Wt::No);
|
||||
|
||||
messageBox->setModal(true);
|
||||
|
||||
messageBox->buttonClicked().connect(std::bind([=] ()
|
||||
{
|
||||
if (messageBox->buttonResult() == Wt::Yes)
|
||||
_playQueue->delAll();
|
||||
|
||||
delete messageBox;
|
||||
}));
|
||||
|
||||
messageBox->show();
|
||||
}));
|
||||
|
||||
playQueueLayout->addLayout(playlistControls);
|
||||
|
||||
mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
|
||||
|
||||
// Load the last known queue
|
||||
playlistLoadToPlayqueue(CurrentQueuePlaylistName);
|
||||
|
||||
// Select the last known playing track
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
|
||||
}
|
||||
|
||||
_mediaPlayer = new AudioPlayer(AudioPlayer::ControlShuffle | AudioPlayer::ControlRepeat);
|
||||
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
|
||||
|
||||
mainLayout->setRowStretch(1, 1);
|
||||
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
|
||||
mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel));
|
||||
|
||||
// Double click on track
|
||||
// Set the selected tracks to the play queue
|
||||
_trackView->trackDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddSelectedTracks));
|
||||
|
||||
// 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));
|
||||
filterCluster->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks));
|
||||
|
||||
// Play button
|
||||
// Set the selected tracks to the play queue
|
||||
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(this, &Audio::addSelectedTracks);
|
||||
|
||||
_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->shuffle().connect(boost::bind(&PlayQueue::setShuffle, _playQueue, _1));
|
||||
_mediaPlayer->loop().connect(boost::bind(&PlayQueue::setLoop,_playQueue, _1));
|
||||
|
||||
_playQueue->tracksUpdated().connect(std::bind([=] () {
|
||||
LMS_LOG(UI, INFO) << "Playqueue updated!";
|
||||
|
||||
playlistSaveFromPlayqueue(CurrentQueuePlaylistName);
|
||||
}));
|
||||
|
||||
|
||||
playlistRefreshMenus();
|
||||
|
||||
// Initially, search for everything
|
||||
_filterChain.searchKeyword("");
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playlistShowSaveNewDialog()
|
||||
{
|
||||
Wt::WDialog *dialog = new Wt::WDialog("New playlist");
|
||||
|
||||
Wt::WLabel *label = new Wt::WLabel("Name", dialog->contents());
|
||||
Wt::WLineEdit *edit = new Wt::WLineEdit(dialog->contents());
|
||||
label->setBuddy(edit);
|
||||
|
||||
Wt::WLengthValidator* validator = new Wt::WLengthValidator();
|
||||
validator->setMinimumLength(3);
|
||||
validator->setMandatory(true);
|
||||
edit->setValidator(validator);
|
||||
|
||||
Wt::WPushButton *save = new Wt::WPushButton("Save", dialog->footer());
|
||||
save->setStyleClass("btn-success");
|
||||
save->setDefault(true);
|
||||
save->disable();
|
||||
|
||||
Wt::WPushButton *cancel = new Wt::WPushButton("Cancel", dialog->footer());
|
||||
dialog->rejectWhenEscapePressed();
|
||||
|
||||
edit->keyWentUp().connect(std::bind([=] () {
|
||||
save->setDisabled(edit->validate() != Wt::WValidator::Valid);
|
||||
}));
|
||||
|
||||
save->clicked().connect(std::bind([=] ()
|
||||
{
|
||||
if (edit->validate())
|
||||
dialog->accept();
|
||||
}));
|
||||
|
||||
cancel->clicked().connect(dialog, &Wt::WDialog::reject);
|
||||
|
||||
dialog->finished().connect(std::bind([=] ()
|
||||
{
|
||||
if (dialog->result() == Wt::WDialog::Accepted)
|
||||
{
|
||||
playlistShowSaveDialog(edit->text().toUTF8());
|
||||
}
|
||||
|
||||
delete dialog;
|
||||
}));
|
||||
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playlistShowSaveDialog(std::string playlistName)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Actually create the dialog only if the given list already exists
|
||||
if (Playlist::get(DboSession(), playlistName, CurrentUser()))
|
||||
{
|
||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
||||
("Overwrite playlist",
|
||||
Wt::WString( "Overwrite playlist '{1}'?").arg(playlistName),
|
||||
Wt::Question, Wt::Yes | Wt::No);
|
||||
|
||||
messageBox->setModal(true);
|
||||
|
||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
||||
if (messageBox->buttonResult() == Wt::Yes)
|
||||
playlistSaveFromPlayqueue(playlistName);
|
||||
|
||||
delete messageBox;
|
||||
}));
|
||||
|
||||
messageBox->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
playlistSaveFromPlayqueue(playlistName);
|
||||
playlistRefreshMenus();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playlistSaveFromPlayqueue(std::string playlistName)
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Saving playqueue to playlist '" << playlistName << "'";
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Playlist::pointer playlist = Playlist::get(DboSession(), playlistName, CurrentUser());
|
||||
if (playlist)
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Erasing playlist '" << playlistName << "'";
|
||||
playlist.remove();
|
||||
}
|
||||
|
||||
playlist = Playlist::create(DboSession(), playlistName, false, CurrentUser());
|
||||
|
||||
std::vector<Track::id_type> trackIds;
|
||||
_playQueue->getTracks(trackIds);
|
||||
|
||||
int pos = 0;
|
||||
for (Track::id_type trackId : trackIds)
|
||||
{
|
||||
Track::pointer track = Track::getById(DboSession(), trackId);
|
||||
|
||||
if (track)
|
||||
PlaylistEntry::create(DboSession(), track, playlist, pos++);
|
||||
}
|
||||
|
||||
LMS_LOG(UI, INFO) << "Saving playqueue to playlist '" << playlistName << "' done. Contains " << pos << " entries";
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playlistLoadToPlayqueue(std::string playlistName)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Loading playlist '" << playlistName << "' to playqueue";
|
||||
|
||||
std::vector<Track::id_type> entries;
|
||||
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Playlist::pointer playlist = Playlist::get(DboSession(), playlistName, CurrentUser());
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
entries = PlaylistEntry::getEntries(DboSession(), playlist);
|
||||
}
|
||||
|
||||
_playQueue->clear();
|
||||
_playQueue->addTracks(entries);
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Loading playlist '" << playlistName << "' to playqueue done. " << entries.size() << " entries";
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Audio::playlistShowDeleteDialog(std::string name)
|
||||
{
|
||||
|
||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
||||
("Delete playlist",
|
||||
Wt::WString( "Deleting playlist '{1}'?").arg(name),
|
||||
Wt::Question, Wt::Yes | Wt::No);
|
||||
|
||||
messageBox->setModal(true);
|
||||
|
||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
||||
if (messageBox->buttonResult() == Wt::Yes)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Playlist::pointer playlist = Playlist::get(DboSession(), name, CurrentUser());
|
||||
if (playlist)
|
||||
playlist.remove();
|
||||
|
||||
playlistRefreshMenus();
|
||||
}
|
||||
|
||||
delete messageBox;
|
||||
}));
|
||||
|
||||
messageBox->show();
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playlistRefreshMenus()
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Clear playlists in each menu
|
||||
LMS_LOG(UI, DEBUG) << "Save item count: " << _popupMenuSave->count();
|
||||
|
||||
WPopupMenuClear(_popupMenuDelete);
|
||||
WPopupMenuClear(_popupMenuLoad);
|
||||
WPopupMenuClear(_popupMenuSave);
|
||||
|
||||
_popupMenuSave->addItem("New")->triggered().connect(std::bind([=] ()
|
||||
{
|
||||
playlistShowSaveNewDialog();
|
||||
}));
|
||||
_popupMenuSave->addSeparator();
|
||||
|
||||
std::vector<Playlist::pointer> playlists = Playlist::get(DboSession(), CurrentUser());
|
||||
|
||||
for (Playlist::pointer playlist : playlists)
|
||||
{
|
||||
if (playlist->getName() == CurrentQueuePlaylistName)
|
||||
continue;
|
||||
|
||||
// Add playlists in each menu
|
||||
_popupMenuDelete->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
||||
{
|
||||
playlistShowDeleteDialog(playlist->getName());
|
||||
}));
|
||||
|
||||
_popupMenuLoad->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
||||
{
|
||||
playlistLoadToPlayqueue(playlist->getName());
|
||||
playlistRefreshMenus(); // in case deleted in other session
|
||||
}));
|
||||
|
||||
_popupMenuSave->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
||||
{
|
||||
playlistShowSaveDialog(playlist->getName());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Audio::search(std::string searchText)
|
||||
{
|
||||
_filterChain.searchKeyword(searchText);
|
||||
}
|
||||
|
||||
void
|
||||
Audio::addSelectedTracks(void)
|
||||
{
|
||||
std::vector<Track::id_type> trackIds;
|
||||
_trackView->getSelectedTracks(trackIds);
|
||||
|
||||
// If nothing is selected, get the whole track list
|
||||
if (trackIds.empty())
|
||||
_trackView->getTracks(trackIds);
|
||||
|
||||
_playQueue->addTracks(trackIds);
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playSelectedTracks(PlayQueueAddType addType)
|
||||
{
|
||||
std::vector<Track::id_type> trackIds;
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Playing selected tracks... nb selected = " << _trackView->getNbSelectedTracks() << ", add type = " << (addType == PlayQueueAddAllTracks ? "AddAll" : "AddSelected");
|
||||
|
||||
_playQueue->clear();
|
||||
|
||||
switch(addType)
|
||||
{
|
||||
case PlayQueueAddAllTracks:
|
||||
// Play all the tracks
|
||||
_trackView->getTracks(trackIds);
|
||||
_playQueue->addTracks(trackIds);
|
||||
_playQueue->play();
|
||||
|
||||
break;
|
||||
case PlayQueueAddSelectedTracks:
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Adding selected tracks...";
|
||||
|
||||
// If nothing selected, get all the track and play everything
|
||||
if (_trackView->getNbSelectedTracks() == 0)
|
||||
{
|
||||
_trackView->getTracks(trackIds);
|
||||
_playQueue->addTracks(trackIds);
|
||||
_playQueue->play();
|
||||
}
|
||||
// If only ONE selected, get them all and pre select the track
|
||||
else if (_trackView->getNbSelectedTracks() == 1)
|
||||
{
|
||||
// Start to play at the selected track, if any
|
||||
_trackView->getTracks(trackIds);
|
||||
_playQueue->addTracks(trackIds);
|
||||
_playQueue->play(_trackView->getFirstSelectedTrackPosition());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Play all the selected tracks
|
||||
_trackView->getSelectedTracks(trackIds);
|
||||
_playQueue->addTracks(trackIds);
|
||||
_playQueue->play();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Audio::playTrack(Track::id_type trackId, int pos)
|
||||
{
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Update user current track position
|
||||
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
||||
}
|
||||
|
||||
if (!_mediaPlayer->loadTrack(trackId))
|
||||
_playQueue->playNext();
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,81 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <Wt/WPopupMenu>
|
||||
|
||||
#include "audio/AudioPlayer.hpp"
|
||||
#include "TrackView.hpp"
|
||||
#include "PlayQueue.hpp"
|
||||
|
||||
#include "FilterChain.hpp"
|
||||
|
||||
#include "audio/Audio.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class Audio : public UserInterface::Audio
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
Audio(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
void search(std::string searchText);
|
||||
|
||||
private:
|
||||
|
||||
void playlistSaveFromPlayqueue(std::string name);
|
||||
void playlistLoadToPlayqueue(std::string name);
|
||||
void playlistShowSaveNewDialog();
|
||||
void playlistShowSaveDialog(std::string name);
|
||||
void playlistShowDeleteDialog(std::string name);
|
||||
void playlistRefreshMenus();
|
||||
|
||||
void playTrack(Database::Track::id_type id, int pos);
|
||||
|
||||
enum PlayQueueAddType
|
||||
{
|
||||
PlayQueueAddAllTracks,
|
||||
PlayQueueAddSelectedTracks,
|
||||
};
|
||||
|
||||
void playSelectedTracks(PlayQueueAddType addType);
|
||||
void addSelectedTracks();
|
||||
|
||||
void handlePlaylistSelected(Wt::WString name);
|
||||
|
||||
AudioPlayer* _mediaPlayer;
|
||||
TrackView* _trackView;
|
||||
PlayQueue* _playQueue;
|
||||
|
||||
FilterChain _filterChain;
|
||||
|
||||
Wt::WPopupMenu* _popupMenuSave;
|
||||
Wt::WPopupMenu* _popupMenuLoad;
|
||||
Wt::WPopupMenu* _popupMenuDelete;
|
||||
};
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,57 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef FILTER_HPP
|
||||
#define FILTER_HPP
|
||||
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class Filter
|
||||
{
|
||||
public:
|
||||
Filter() {}
|
||||
virtual ~Filter() {}
|
||||
|
||||
// Refresh filter using constraints created by parent filters
|
||||
virtual void refresh(Database::SearchFilter& filter) = 0;
|
||||
|
||||
// Update constraints for next filters
|
||||
virtual void getConstraint(Database::SearchFilter& filter) = 0;
|
||||
|
||||
// Emitted when a constraint has changed
|
||||
Wt::Signal<void>& update() { return _update; };
|
||||
|
||||
protected:
|
||||
|
||||
void emitUpdate() { _update.emit(); }
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _update;
|
||||
};
|
||||
|
||||
} // namespace Dekstop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FilterChain.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
|
||||
FilterChain::FilterChain()
|
||||
: _refreshingFilters(false)
|
||||
{
|
||||
addFilter(&_keywordSearchFilter);
|
||||
}
|
||||
|
||||
void
|
||||
FilterChain::addFilter(Filter* filter)
|
||||
{
|
||||
filter->update().connect(boost::bind(&FilterChain::updateFilters, this, _filters.size()));
|
||||
_filters.push_back(filter);
|
||||
}
|
||||
|
||||
void
|
||||
FilterChain::searchKeyword(const std::string& text)
|
||||
{
|
||||
_keywordSearchFilter.setText(text);
|
||||
}
|
||||
|
||||
void
|
||||
FilterChain::updateFilters(std::size_t startIdx)
|
||||
{
|
||||
// Prevent loops
|
||||
if (_refreshingFilters)
|
||||
return;
|
||||
|
||||
_refreshingFilters = true;
|
||||
|
||||
Database::SearchFilter searchFilter;
|
||||
|
||||
for (std::size_t idFilter = 0; idFilter < _filters.size(); ++idFilter)
|
||||
{
|
||||
Filter* filter = _filters.at(idFilter);
|
||||
|
||||
// Apply contraints created by previous filters
|
||||
if (idFilter > startIdx) {
|
||||
filter->refresh(searchFilter);
|
||||
}
|
||||
|
||||
// Get constraints generated by this filter
|
||||
// (Note: adding accross successive calls)
|
||||
filter->getConstraint(searchFilter);
|
||||
}
|
||||
|
||||
_refreshingFilters = false;
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef FILTER_CHAIN_HPP
|
||||
#define FILTER_CHAIN_HPP
|
||||
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "Filter.hpp"
|
||||
#include "KeywordSearchFilter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
// FilterChain
|
||||
class FilterChain
|
||||
{
|
||||
public:
|
||||
|
||||
FilterChain();
|
||||
|
||||
void addFilter(Filter* filter);
|
||||
|
||||
// First filter is a keywork search
|
||||
void searchKeyword(const std::string& text);
|
||||
|
||||
// Update filters from filter @ startIdx
|
||||
void updateFilters(std::size_t startIdx);
|
||||
|
||||
private:
|
||||
|
||||
KeywordSearchFilter _keywordSearchFilter;
|
||||
|
||||
// No ownership
|
||||
std::vector<Filter*> _filters;
|
||||
|
||||
bool _refreshingFilters;
|
||||
};
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,64 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
#include "KeywordSearchFilter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
KeywordSearchFilter::KeywordSearchFilter()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
KeywordSearchFilter::setText(const std::string& text)
|
||||
{
|
||||
_lastEmittedText = text;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// Get constraints created by this filter
|
||||
void
|
||||
KeywordSearchFilter::getConstraint(Database::SearchFilter& filter)
|
||||
{
|
||||
// No active search means no constaint!
|
||||
if (!_lastEmittedText.empty()) {
|
||||
std::vector<std::string> values;
|
||||
boost::algorithm::split(values, _lastEmittedText, boost::is_any_of(" "), boost::token_compress_on);
|
||||
|
||||
// For each part, do a global search on all searchable fields
|
||||
for (const std::string& value : values)
|
||||
{
|
||||
filter.nameLikeMatch.push_back({
|
||||
{Database::SearchFilter::Field::Artist, {value}},
|
||||
{Database::SearchFilter::Field::Release, {value}},
|
||||
{Database::SearchFilter::Field::Cluster, {value}},
|
||||
{Database::SearchFilter::Field::Track, {value}}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,54 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEYWORD_SEARCH_FILTER_HPP
|
||||
#define KEYWORD_SEARCH_FILTER_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Filter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class KeywordSearchFilter : public Filter
|
||||
{
|
||||
public:
|
||||
KeywordSearchFilter();
|
||||
|
||||
void setText(const std::string& text);
|
||||
|
||||
// Set constraint on this filter
|
||||
void refresh(Database::SearchFilter& filter) {}
|
||||
|
||||
// Get constraints created by this filter
|
||||
void getConstraint(Database::SearchFilter& filter);
|
||||
|
||||
private:
|
||||
|
||||
void handleKeyWentUp(void);
|
||||
|
||||
std::string _lastEmittedText;
|
||||
};
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,634 +0,0 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/random/mersenne_twister.hpp>
|
||||
#include <boost/random/uniform_int_distribution.hpp>
|
||||
|
||||
#include <Wt/WApplication>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WItemDelegate>
|
||||
#include <Wt/WStandardItem>
|
||||
#include <Wt/WFileResource>
|
||||
#include <Wt/WTheme>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "PlayQueue.hpp"
|
||||
|
||||
static const int TrackInfoRole = Wt::UserRole;
|
||||
|
||||
namespace {
|
||||
|
||||
void swapRows(Wt::WStandardItemModel *model, int row1, int row2)
|
||||
{
|
||||
// Swap data column by column
|
||||
for (int i = 0; i < model->columnCount(); ++i)
|
||||
{
|
||||
Wt::WModelIndex index1 = model->index(row1, i);
|
||||
Wt::WModelIndex index2 = model->index(row2, i);
|
||||
|
||||
// swap data associated with standard roles
|
||||
{
|
||||
auto tmp = model->itemData(index1); // 1 -> tmp
|
||||
model->setItemData(index1, model->itemData(index2)); // 2 -> 1
|
||||
model->setItemData(index2, tmp); // tmp-> 2
|
||||
}
|
||||
// caution: swap data associated with our custom roles if any!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
enum ColumnId
|
||||
{
|
||||
COLUMN_ID_TRACK_ID = 0,
|
||||
COLUMN_ID_COVER = 1,
|
||||
COLUMN_ID_NAME = 2,
|
||||
};
|
||||
|
||||
static const int trackPosInvalid = -1;
|
||||
|
||||
class TrackSelector
|
||||
{
|
||||
public:
|
||||
TrackSelector();
|
||||
|
||||
void setShuffle(bool enable);
|
||||
void setLoop(bool enable) { _loop = enable; }
|
||||
|
||||
int previous(void);
|
||||
int next(void);
|
||||
int getCurrent(void);
|
||||
|
||||
// Set the internal pos thanks to the track pos
|
||||
void setPosByRowId(int rowId);
|
||||
void setPos(int pos) { _curPos = pos; }
|
||||
|
||||
void setSize(std::size_t size);
|
||||
std::size_t getSize(void) const { return _size;}
|
||||
|
||||
private:
|
||||
|
||||
void refreshPositions();
|
||||
|
||||
bool _loop;
|
||||
bool _shuffle;
|
||||
|
||||
std::size_t _size;
|
||||
std::size_t _curPos;
|
||||
|
||||
std::vector<int> _trackPos;
|
||||
};
|
||||
|
||||
TrackSelector::TrackSelector()
|
||||
: _loop(false),
|
||||
_shuffle(false),
|
||||
_size(0),
|
||||
_curPos(0)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
TrackSelector::refreshPositions()
|
||||
{
|
||||
_trackPos.clear();
|
||||
|
||||
if (_size == 0)
|
||||
return;
|
||||
|
||||
for (std::size_t i = 0; i < _size; ++i)
|
||||
_trackPos.push_back(i);
|
||||
|
||||
// Now shuffle
|
||||
// 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<unsigned int>(std::time(0)));
|
||||
|
||||
for (std::size_t i = 0; i < _size; ++i)
|
||||
{
|
||||
boost::random::uniform_int_distribution<> distribution(i, _size - 1);
|
||||
int val1 = distribution(rng);
|
||||
int tmp = _trackPos[i];
|
||||
_trackPos[i] = _trackPos[val1];
|
||||
_trackPos[val1] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
TrackSelector::getCurrent()
|
||||
{
|
||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
||||
}
|
||||
|
||||
void
|
||||
TrackSelector::setShuffle(bool enable)
|
||||
{
|
||||
_shuffle = enable;
|
||||
|
||||
if (_size != 0)
|
||||
{
|
||||
// Update the current index we are playing if we switch shuffle
|
||||
if (_shuffle)
|
||||
setPosByRowId(_curPos);
|
||||
else
|
||||
setPos(_trackPos[_curPos]);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
TrackSelector::next()
|
||||
{
|
||||
if (_size == 0)
|
||||
return trackPosInvalid;
|
||||
|
||||
if (_curPos == _size - 1)
|
||||
{
|
||||
if (!_loop)
|
||||
return trackPosInvalid;
|
||||
else
|
||||
_curPos = 0;
|
||||
}
|
||||
else
|
||||
++_curPos;
|
||||
|
||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
||||
}
|
||||
|
||||
int
|
||||
TrackSelector::previous()
|
||||
{
|
||||
if (_size == 0)
|
||||
return trackPosInvalid;
|
||||
|
||||
if (_curPos == 0)
|
||||
{
|
||||
if (!_loop)
|
||||
return trackPosInvalid;
|
||||
else
|
||||
_curPos = _size - 1;
|
||||
}
|
||||
else
|
||||
--_curPos;
|
||||
|
||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
||||
}
|
||||
|
||||
void
|
||||
TrackSelector::setPosByRowId(int rowId)
|
||||
{
|
||||
if (_shuffle)
|
||||
{
|
||||
for (std::size_t i = 0; i < _trackPos.size(); ++i)
|
||||
{
|
||||
if (rowId == _trackPos[i])
|
||||
{
|
||||
_curPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
_curPos = rowId;
|
||||
}
|
||||
|
||||
void
|
||||
TrackSelector::setSize(std::size_t size)
|
||||
{
|
||||
_size = size;
|
||||
_curPos = 0;
|
||||
refreshPositions();
|
||||
}
|
||||
|
||||
struct TrackInfo
|
||||
{
|
||||
Wt::WString track;
|
||||
Wt::WString artist;
|
||||
Wt::WString release;
|
||||
};
|
||||
|
||||
class PlayQueueItemDelegate : public Wt::WItemDelegate
|
||||
{
|
||||
public:
|
||||
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;
|
||||
|
||||
if (!index.data(TrackInfoRole).empty())
|
||||
{
|
||||
TrackInfo trackInfo = boost::any_cast<TrackInfo>(index.data(TrackInfoRole));
|
||||
|
||||
Wt::WContainerWidget *container = new Wt::WContainerWidget();
|
||||
Wt::WText* track = new Wt::WText(trackInfo.track, Wt::PlainText, container);
|
||||
Wt::WText* artist = new Wt::WText(trackInfo.artist + " - " + trackInfo.release, Wt::PlainText, container);
|
||||
|
||||
artist->setInline(false);
|
||||
track->setInline(false);
|
||||
|
||||
artist->setStyleClass("playqueue-artist");
|
||||
track->setStyleClass("playqueue-track");
|
||||
|
||||
// Apply style if any
|
||||
Wt::WString styleClass = Wt::asString(index.data(Wt::StyleClassRole));
|
||||
|
||||
// Apply selection style if any
|
||||
if (flags & Wt::RenderSelected)
|
||||
styleClass += " " + Wt::WApplication::instance()->theme()->activeClass();
|
||||
|
||||
container->setStyleClass(styleClass);
|
||||
|
||||
res = container;
|
||||
}
|
||||
else
|
||||
res = Wt::WItemDelegate::update(widget, index, flags);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
|
||||
PlayQueue::PlayQueue(Wt::WContainerWidget* parent)
|
||||
: Wt::WTableView(parent),
|
||||
_curPlayedTrackPos(trackPosInvalid),
|
||||
_trackSelector(new TrackSelector())
|
||||
{
|
||||
_model = new Wt::WStandardItemModel(0, 3, this);
|
||||
|
||||
// 0 Column is hidden (track id)
|
||||
|
||||
this->setHeaderHeight(0);
|
||||
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_NAME, 240);
|
||||
|
||||
this->setLayoutSizeAware(true);
|
||||
#if WT_VERSION >= 0X03030400
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
||||
#endif
|
||||
this->setColumnHidden(COLUMN_ID_TRACK_ID, true);
|
||||
|
||||
_itemDelegate = new PlayQueueItemDelegate();
|
||||
this->setItemDelegate(_itemDelegate);
|
||||
|
||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
||||
{
|
||||
if (!idx.isValid())
|
||||
return;
|
||||
|
||||
// Update selection
|
||||
Wt::WModelIndexSet indexSet;
|
||||
indexSet.insert(idx);
|
||||
|
||||
this->setSelectedIndexes( indexSet );
|
||||
|
||||
// Read the requested track
|
||||
play(idx.row());
|
||||
|
||||
}, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::layoutSizeChanged (int width, int height)
|
||||
{
|
||||
std::size_t coverColumnSize = this->columnWidth(COLUMN_ID_COVER).toPixels();
|
||||
// Set the remaining size for the name column
|
||||
this->setColumnWidth(COLUMN_ID_NAME, width - coverColumnSize - (7 * 2) - 2);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::setShuffle(bool enable)
|
||||
{
|
||||
_trackSelector->setShuffle(enable);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::setLoop(bool enable)
|
||||
{
|
||||
_trackSelector->setLoop(enable);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::play()
|
||||
{
|
||||
_trackSelector->setPos(0);
|
||||
|
||||
readTrack(_trackSelector->getCurrent());
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::play(int rowId)
|
||||
{
|
||||
// Update the track selector to use the requested track as current position
|
||||
_trackSelector->setPosByRowId(rowId);
|
||||
|
||||
readTrack(_trackSelector->getCurrent());
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::select(int rowId)
|
||||
{
|
||||
// Update the track selector to use the requested track as current position
|
||||
_trackSelector->setPosByRowId(rowId);
|
||||
|
||||
Wt::WTableView::select(_model->index(_trackSelector->getCurrent(), 0));
|
||||
|
||||
this->scrollTo( _model->index(_trackSelector->getCurrent(), 0));
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::addTracks(const std::vector<Database::Track::id_type>& trackIds)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Adding " << trackIds.size() << " tracks to play queue";
|
||||
|
||||
// Add tracks to model
|
||||
for (Track::id_type trackId : trackIds)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Track::pointer track (Track::getById(DboSession(), trackId));
|
||||
|
||||
if (track)
|
||||
{
|
||||
int dataRow = _model->rowCount();
|
||||
|
||||
_model->insertRows(dataRow, 1);
|
||||
|
||||
_model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id(), Wt::UserRole);
|
||||
|
||||
std::string coverUrl;
|
||||
coverUrl = SessionImageResource()->getTrackUrl(track.id(), 64);
|
||||
|
||||
_model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole);
|
||||
_model->setData(dataRow, COLUMN_ID_COVER, std::string("playqueue-cover"), Wt::StyleClassRole);
|
||||
|
||||
TrackInfo trackInfo;
|
||||
trackInfo.track = Wt::WString::fromUTF8(track->getName());
|
||||
trackInfo.artist = Wt::WString::fromUTF8(track->getArtist()->getName());
|
||||
trackInfo.release = Wt::WString::fromUTF8(track->getRelease()->getName());
|
||||
_model->setData(dataRow, COLUMN_ID_NAME, trackInfo, TrackInfoRole);
|
||||
}
|
||||
}
|
||||
|
||||
_trackSelector->setSize( _model->rowCount() );
|
||||
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::clear(void)
|
||||
{
|
||||
_model->removeRows(0, _model->rowCount());
|
||||
|
||||
// Reset play id
|
||||
_curPlayedTrackPos = trackPosInvalid;
|
||||
_trackSelector->setSize( 0 );
|
||||
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::handlePlaybackComplete(void)
|
||||
{
|
||||
playNext();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::playNext(void)
|
||||
{
|
||||
std::size_t nbTries = _trackSelector->getSize();
|
||||
|
||||
while (nbTries > 0)
|
||||
{
|
||||
int pos = _trackSelector->next();
|
||||
if (pos == trackPosInvalid)
|
||||
break;
|
||||
|
||||
readTrack(pos);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::playPrevious(void)
|
||||
{
|
||||
std::size_t nbTries = _trackSelector->getSize();
|
||||
|
||||
while (nbTries > 0)
|
||||
{
|
||||
int pos = _trackSelector->previous();
|
||||
if (pos == trackPosInvalid)
|
||||
break;
|
||||
|
||||
readTrack(pos);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::readTrack(int rowPos)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Reading track at pos " << rowPos << ", row count = " << _model->rowCount();
|
||||
|
||||
if (rowPos < _model->rowCount())
|
||||
{
|
||||
Database::Track::id_type trackId = boost::any_cast<Database::Track::id_type>(_model->data(rowPos, COLUMN_ID_TRACK_ID, Wt::UserRole));
|
||||
|
||||
setPlayingTrackPos(rowPos);
|
||||
|
||||
_sigTrackPlay.emit(trackId, rowPos);
|
||||
|
||||
this->scrollTo( _model->index(_trackSelector->getCurrent(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::setPlayingTrackPos(int newRowPos)
|
||||
{
|
||||
int oldRowPos = _curPlayedTrackPos;
|
||||
_curPlayedTrackPos = 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)
|
||||
{
|
||||
_model->setData(oldRowPos, COLUMN_ID_NAME, boost::any(), Wt::StyleClassRole);
|
||||
}
|
||||
|
||||
if (newRowPos >= 0)
|
||||
{
|
||||
_model->setData(newRowPos, COLUMN_ID_NAME, std::string("playqueue-playing"), Wt::StyleClassRole);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::delSelected(void)
|
||||
{
|
||||
int minId = _model->rowCount();
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
// Make sure to delete entries in the reverse order
|
||||
std::vector<int> rowIds;
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
rowIds.push_back(index.row());
|
||||
|
||||
std::sort(rowIds.begin(), rowIds.end(), std::greater<int>());
|
||||
if (!rowIds.empty())
|
||||
minId = rowIds.back();
|
||||
|
||||
for (int rowId : rowIds)
|
||||
_model->removeRow(rowId);
|
||||
|
||||
// If the current played track is removed, make sure to unselect it
|
||||
_trackSelector->setSize(_model->rowCount());
|
||||
|
||||
renumber(minId, _model->rowCount() - 1);
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::delAll(void)
|
||||
{
|
||||
_model->removeRows(0, _model->rowCount());
|
||||
_trackSelector->setSize(0);
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::moveSelectedUp(void)
|
||||
{
|
||||
int minId = _model->rowCount();
|
||||
int maxId = 0;
|
||||
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
Wt::WModelIndexSet newIndexSet;
|
||||
|
||||
// ordered from up to down
|
||||
for (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 (_curPlayedTrackPos == index.row())
|
||||
setPlayingTrackPos(_curPlayedTrackPos - 1);
|
||||
else if (_curPlayedTrackPos == index.row() - 1)
|
||||
setPlayingTrackPos(_curPlayedTrackPos + 1);
|
||||
|
||||
newIndexSet.insert( _model->index(index.row() - 1, 0));
|
||||
}
|
||||
|
||||
_trackSelector->setPosByRowId(_curPlayedTrackPos);
|
||||
this->setSelectedIndexes( newIndexSet );
|
||||
|
||||
renumber(minId, maxId);
|
||||
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
PlayQueue::moveSelectedDown(void)
|
||||
{
|
||||
int minId = _model->rowCount();
|
||||
int maxId = 0;
|
||||
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
Wt::WModelIndexSet newIndexSet;
|
||||
|
||||
// ordered from up to down
|
||||
for (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 (_curPlayedTrackPos == index.row())
|
||||
setPlayingTrackPos(_curPlayedTrackPos + 1);
|
||||
else if (_curPlayedTrackPos == index.row() + 1)
|
||||
setPlayingTrackPos(_curPlayedTrackPos - 1);
|
||||
|
||||
newIndexSet.insert( _model->index(index.row() + 1, 0));
|
||||
}
|
||||
|
||||
_trackSelector->setPosByRowId(_curPlayedTrackPos);
|
||||
this->setSelectedIndexes( newIndexSet );
|
||||
|
||||
renumber(minId, maxId);
|
||||
|
||||
_sigTracksUpdated.emit();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::renumber(int firstId, int lastId)
|
||||
{
|
||||
for (int i = firstId; i <= lastId; ++i)
|
||||
_model->setData( i, 1, i + 1);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::getTracks(std::vector<Database::Track::id_type>& trackIds) const
|
||||
{
|
||||
// Now add each entry in the playlist
|
||||
for (int i = 0; i < _model->rowCount(); ++i)
|
||||
{
|
||||
Database::Track::id_type trackId = boost::any_cast<Database::Track::id_type>(_model->data(i, COLUMN_ID_TRACK_ID, Wt::UserRole));
|
||||
trackIds.push_back(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef PLAY_QUEUE_HPP
|
||||
#define PLAY_QUEUE_HPP
|
||||
|
||||
#include <Wt/WTableView>
|
||||
#include <Wt/WStandardItemModel>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class PlayQueueItemDelegate;
|
||||
class TrackSelector;
|
||||
|
||||
class PlayQueue : public Wt::WTableView
|
||||
{
|
||||
public:
|
||||
PlayQueue(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
void addTracks(const std::vector<Database::Track::id_type>& trackIds);
|
||||
void getTracks(std::vector<Database::Track::id_type>& trackIds) const;
|
||||
|
||||
void clear(void);
|
||||
|
||||
void setShuffle(bool enable); // true to enable shuffle
|
||||
void setLoop(bool enable); // true to enable loop
|
||||
|
||||
// Play functions
|
||||
void play(void); // Play the queue from the beginning
|
||||
void play(int rowId); // Play the queue from the given rowId
|
||||
void select(int rowId); // Select the given rowId
|
||||
void playNext(void); // Play the next track
|
||||
void playPrevious(void); // Play the previous track
|
||||
|
||||
// List manipulations
|
||||
void delSelected(void);
|
||||
void delAll(void);
|
||||
void moveSelectedUp(void);
|
||||
void moveSelectedDown(void);
|
||||
|
||||
// Signals
|
||||
// Emitted when a song has to be played
|
||||
Wt::Signal< Database::Track::id_type, int >& playTrack() { return _sigTrackPlay; }
|
||||
// Emitted when the list has changed
|
||||
Wt::Signal< void >& tracksUpdated() { return _sigTracksUpdated; }
|
||||
|
||||
// Slots
|
||||
void handlePlaybackComplete(void);
|
||||
|
||||
private:
|
||||
|
||||
void layoutSizeChanged (int width, int height);
|
||||
|
||||
void readTrack(int rowId);
|
||||
void setPlayingTrackPos(int newRowPos);
|
||||
void renumber(int firstId, int lastId);
|
||||
|
||||
Wt::Signal< Database::Track::id_type, int > _sigTrackPlay;
|
||||
Wt::Signal< void > _sigTracksUpdated;
|
||||
|
||||
Wt::WStandardItemModel* _model;
|
||||
|
||||
PlayQueueItemDelegate* _itemDelegate;
|
||||
|
||||
int _curPlayedTrackPos;
|
||||
std::unique_ptr<TrackSelector> _trackSelector;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,282 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WConfig.h>
|
||||
#include <Wt/WItemDelegate>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "TableFilter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TableFilterCluster::TableFilterCluster(Wt::WContainerWidget* parent)
|
||||
: Wt::WTableView( parent ), Filter()
|
||||
{
|
||||
const std::vector<Wt::WString> columnNames = {"Tag", "Tracks"};
|
||||
|
||||
SearchFilter filter;
|
||||
|
||||
Cluster::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
||||
|
||||
this->setSelectionMode(Wt::ExtendedSelection);
|
||||
this->setSortingEnabled(true);
|
||||
this->setAlternatingRowColors(true);
|
||||
this->setModel(&_queryModel);
|
||||
|
||||
this->setColumnWidth(1, 80);
|
||||
|
||||
this->selectionChanged().connect(this, &TableFilterCluster::emitUpdate);
|
||||
|
||||
#if WT_VERSION >= 0X03030400
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
||||
#endif
|
||||
|
||||
this->setLayoutSizeAware(true);
|
||||
|
||||
_queryModel.setBatchSize(150);
|
||||
|
||||
// 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
|
||||
TableFilterCluster::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 - trackColumnSize - (7 * 2) - 2);
|
||||
}
|
||||
|
||||
// Set constraints on this filter
|
||||
void
|
||||
TableFilterCluster::refresh(SearchFilter& filter)
|
||||
{
|
||||
#if WT_VERSION >= 0X03030500
|
||||
this->clearSelection();
|
||||
#endif
|
||||
|
||||
Cluster::updateUIQueryModel(DboSession(), _queryModel, filter);
|
||||
}
|
||||
|
||||
// Get constraint created by this filter
|
||||
void
|
||||
TableFilterCluster::getConstraint(SearchFilter& filter)
|
||||
{
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
{
|
||||
if (!index.isValid())
|
||||
continue;
|
||||
|
||||
// TODO set invisible track id
|
||||
Cluster::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
||||
|
||||
filter.idMatch[Database::SearchFilter::Field::Cluster].push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
TableFilterArtist::TableFilterArtist(Wt::WContainerWidget* parent)
|
||||
: Wt::WTableView( parent ), Filter()
|
||||
{
|
||||
const std::vector<Wt::WString> columnNames = {"Artist", "Releases", "Tracks"};
|
||||
|
||||
SearchFilter filter;
|
||||
|
||||
Artist::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
||||
|
||||
this->setSelectionMode(Wt::ExtendedSelection);
|
||||
this->setSortingEnabled(true);
|
||||
this->setAlternatingRowColors(true);
|
||||
this->setModel(&_queryModel);
|
||||
|
||||
this->setColumnWidth(1, 80);
|
||||
this->setColumnWidth(2, 80);
|
||||
|
||||
this->selectionChanged().connect(this, &TableFilterArtist::emitUpdate);
|
||||
|
||||
#if WT_VERSION >= 0X03030400
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
||||
#endif
|
||||
|
||||
this->setLayoutSizeAware(true);
|
||||
|
||||
_queryModel.setBatchSize(150);
|
||||
|
||||
// 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
|
||||
TableFilterArtist::layoutSizeChanged (int width, int height)
|
||||
{
|
||||
std::size_t otherColumnsSize = this->columnWidth(1).toPixels() + this->columnWidth(2).toPixels();
|
||||
// Set the remaining size for the name column
|
||||
this->setColumnWidth(0, width - otherColumnsSize - (7 * 3) - 2);
|
||||
}
|
||||
|
||||
// Set constraints on this filter
|
||||
void
|
||||
TableFilterArtist::refresh(SearchFilter& filter)
|
||||
{
|
||||
#if WT_VERSION >= 0X03030500
|
||||
this->clearSelection();
|
||||
#endif
|
||||
Artist::updateUIQueryModel(DboSession(), _queryModel, filter);
|
||||
}
|
||||
|
||||
// Get constraint created by this filter
|
||||
void
|
||||
TableFilterArtist::getConstraint(SearchFilter& filter)
|
||||
{
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
{
|
||||
if (!index.isValid())
|
||||
continue;
|
||||
|
||||
Artist::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
||||
|
||||
filter.idMatch[SearchFilter::Field::Artist].push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
TableFilterRelease::TableFilterRelease(Wt::WContainerWidget* parent)
|
||||
: Wt::WTableView( parent ), Filter()
|
||||
{
|
||||
const std::vector<Wt::WString> columnNames = {"Release", "Date", "Tracks"};
|
||||
|
||||
SearchFilter filter;
|
||||
|
||||
Release::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
||||
|
||||
this->setSelectionMode(Wt::ExtendedSelection);
|
||||
this->setSortingEnabled(true);
|
||||
this->setAlternatingRowColors(true);
|
||||
this->setModel(&_queryModel);
|
||||
|
||||
this->setColumnWidth(1, 60);
|
||||
this->setColumnWidth(2, 80);
|
||||
|
||||
// Date display, just the year
|
||||
{
|
||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
||||
delegate->setTextFormat("yyyy");
|
||||
this->setItemDelegateForColumn(1, delegate);
|
||||
}
|
||||
|
||||
this->selectionChanged().connect(this, &TableFilterRelease::emitUpdate);
|
||||
|
||||
#if WT_VERSION >= 0X03030400
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
||||
#endif
|
||||
|
||||
this->setLayoutSizeAware(true);
|
||||
|
||||
_queryModel.setBatchSize(150);
|
||||
|
||||
// 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
|
||||
TableFilterRelease::layoutSizeChanged (int width, int height)
|
||||
{
|
||||
std::size_t otherColumnSizes = this->columnWidth(1).toPixels() + this->columnWidth(2).toPixels();
|
||||
// Set the remaining size for the name column
|
||||
this->setColumnWidth(0, width - otherColumnSizes - (7 * 3) - 2);
|
||||
}
|
||||
|
||||
// Set constraints on this filter
|
||||
void
|
||||
TableFilterRelease::refresh(SearchFilter& filter)
|
||||
{
|
||||
#if WT_VERSION >= 0X03030500
|
||||
this->clearSelection();
|
||||
#endif
|
||||
Release::updateUIQueryModel(DboSession(), _queryModel, filter);
|
||||
}
|
||||
|
||||
// Get constraint created by this filter
|
||||
void
|
||||
TableFilterRelease::getConstraint(SearchFilter& filter)
|
||||
{
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
{
|
||||
if (!index.isValid())
|
||||
continue;
|
||||
|
||||
Release::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
||||
|
||||
filter.idMatch[Database::SearchFilter::Field::Release].push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,112 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TABLE_FILTER_HPP
|
||||
#define TABLE_FILTER_HPP
|
||||
|
||||
|
||||
#include <Wt/Dbo/QueryModel>
|
||||
#include <Wt/WTableView>
|
||||
|
||||
#include "Filter.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class TableFilterCluster : public Wt::WTableView, public Filter
|
||||
{
|
||||
public:
|
||||
TableFilterCluster(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
// Set constraints on this filter
|
||||
void refresh(Database::SearchFilter& filter);
|
||||
|
||||
// Get constraints created by this filter
|
||||
void getConstraint(Database::SearchFilter& filter);
|
||||
|
||||
void layoutSizeChanged (int width, int height);
|
||||
|
||||
typedef Wt::Signal<void> SigDoubleClicked;
|
||||
|
||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
||||
|
||||
protected:
|
||||
|
||||
SigDoubleClicked _sigDoubleClicked;
|
||||
|
||||
Wt::Dbo::QueryModel<Database::Cluster::UIQueryResult> _queryModel;
|
||||
};
|
||||
|
||||
class TableFilterArtist : public Wt::WTableView, public Filter
|
||||
{
|
||||
public:
|
||||
TableFilterArtist(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
// Set constraints on this filter
|
||||
void refresh(Database::SearchFilter& filter);
|
||||
|
||||
// Get constraints created by this filter
|
||||
void getConstraint(Database::SearchFilter& filter);
|
||||
|
||||
void layoutSizeChanged (int width, int height);
|
||||
|
||||
typedef Wt::Signal<void> SigDoubleClicked;
|
||||
|
||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
||||
|
||||
protected:
|
||||
|
||||
SigDoubleClicked _sigDoubleClicked;
|
||||
|
||||
// Name, track count
|
||||
Wt::Dbo::QueryModel<Database::Artist::UIQueryResult> _queryModel;
|
||||
};
|
||||
|
||||
class TableFilterRelease : public Wt::WTableView, public Filter
|
||||
{
|
||||
public:
|
||||
TableFilterRelease(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
// Set constraints on this filter
|
||||
void refresh(Database::SearchFilter& filter);
|
||||
|
||||
// Get constraints created by this filter
|
||||
void getConstraint(Database::SearchFilter& filter);
|
||||
|
||||
void layoutSizeChanged (int width, int height);
|
||||
|
||||
typedef Wt::Signal<void> SigDoubleClicked;
|
||||
|
||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
||||
|
||||
protected:
|
||||
|
||||
SigDoubleClicked _sigDoubleClicked;
|
||||
|
||||
// Name, track count
|
||||
Wt::Dbo::QueryModel<Database::Release::UIQueryResult> _queryModel;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,234 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/date_time.hpp>
|
||||
|
||||
#include <Wt/WConfig.h>
|
||||
#include <Wt/WItemDelegate>
|
||||
#include <Wt/WBreak>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "TrackView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
class DurationItemDelegate : public Wt::WItemDelegate
|
||||
{
|
||||
public:
|
||||
DurationItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent) {}
|
||||
|
||||
Wt::WWidget* update(Wt::WWidget *widget, const Wt::WModelIndex &index, Wt::WFlags< Wt::ViewItemRenderFlag > flags)
|
||||
{
|
||||
boost::posix_time::time_duration duration = boost::any_cast<boost::posix_time::time_duration>(index.data(Wt::DisplayRole));
|
||||
|
||||
// TODO bug when redrawn
|
||||
std::string format = duration.total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
||||
|
||||
return new Wt::WText(durationToString(duration, format), Wt::PlainText);
|
||||
}
|
||||
};
|
||||
|
||||
TrackView::TrackView(Wt::WContainerWidget* parent)
|
||||
: Wt::WTableView( parent )
|
||||
{
|
||||
|
||||
static const std::vector<Wt::WString> columnNames =
|
||||
{
|
||||
"Artist",
|
||||
"Album",
|
||||
"Disc #",
|
||||
"Track #",
|
||||
"Track",
|
||||
"Duration",
|
||||
"Date",
|
||||
"Original Date",
|
||||
"Genres",
|
||||
};
|
||||
|
||||
SearchFilter filter;
|
||||
|
||||
Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
||||
|
||||
_queryModel.setBatchSize(500);
|
||||
|
||||
this->setSortingEnabled(true);
|
||||
this->setSelectionMode(Wt::ExtendedSelection);
|
||||
this->setAlternatingRowColors(true);
|
||||
this->setModel(&_queryModel);
|
||||
|
||||
this->setColumnWidth(0, 180); // Artist
|
||||
this->setColumnWidth(1, 180); // Album
|
||||
this->setColumnWidth(2, 70); // Disc Number
|
||||
this->setColumnWidth(3, 70); // Track Number
|
||||
this->setColumnWidth(4, 180); // Track
|
||||
this->setColumnWidth(5, 70); // Duration
|
||||
this->setColumnWidth(6, 70); // Date
|
||||
this->setColumnWidth(7, 70); // Original Date
|
||||
this->setColumnWidth(8, 180); // Genres
|
||||
#if WT_VERSION >= 0X03030400
|
||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
||||
#endif
|
||||
|
||||
// Duration display
|
||||
{
|
||||
// TODO better handle 1 hour+ files!
|
||||
DurationItemDelegate *delegate = new DurationItemDelegate(this);
|
||||
this->setItemDelegateForColumn(5, delegate);
|
||||
}
|
||||
|
||||
// Date display, just the year
|
||||
{
|
||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
||||
delegate->setTextFormat("yyyy");
|
||||
this->setItemDelegateForColumn(6, delegate);
|
||||
}
|
||||
{
|
||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
||||
delegate->setTextFormat("yyyy");
|
||||
this->setItemDelegateForColumn(7, delegate);
|
||||
}
|
||||
|
||||
// If an item is double clicked, select the track 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 );
|
||||
|
||||
_sigTrackDoubleClicked.emit( );
|
||||
|
||||
}, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
TrackView::emitStats(const SearchFilter& filter)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction (DboSession());
|
||||
|
||||
// Update stats on the view
|
||||
Track::StatsQueryResult stats = Track::getStats(DboSession(), filter);
|
||||
|
||||
transaction.commit();
|
||||
|
||||
int nbTracks = stats.get<0>();
|
||||
boost::posix_time::time_duration totalDuration = stats.get<1>();
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << nbTracks << " track" << (nbTracks > 1 ? "s" : "") << ", ";
|
||||
|
||||
if (totalDuration.hours() >= 24)
|
||||
{
|
||||
auto days = totalDuration.hours() / 24;
|
||||
oss << days << " day" << (days > 1 ? "s " : " ");
|
||||
}
|
||||
|
||||
oss << std::setw(2) << std::setfill('0') << totalDuration.hours() % 24
|
||||
<< ":" << std::setw(2) << std::setfill('0') << totalDuration.minutes()
|
||||
<< ":" << std::setw(2) << std::setfill('0') << totalDuration.seconds();
|
||||
|
||||
_sigStatsUpdated.emit(Wt::WString(oss.str()));
|
||||
}
|
||||
|
||||
// Set constraints created by parent filters
|
||||
void
|
||||
TrackView::refresh(SearchFilter& filter)
|
||||
{
|
||||
#if WT_VERSION >= 0X03030500
|
||||
this->clearSelection();
|
||||
#endif
|
||||
Track::updateUIQueryModel(DboSession(), _queryModel, filter);
|
||||
|
||||
emitStats(filter);
|
||||
}
|
||||
|
||||
void
|
||||
TrackView::getSelectedTracks(std::vector<Track::id_type>& track_ids)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Getting selected tracks...";
|
||||
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
{
|
||||
if (!index.isValid())
|
||||
continue;
|
||||
|
||||
Track::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
||||
|
||||
track_ids.push_back(id);
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Getting all selected tracks: " << track_ids.size();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackView::getNbSelectedTracks(void)
|
||||
{
|
||||
return this->selectedIndexes().size();
|
||||
}
|
||||
|
||||
int
|
||||
TrackView::getFirstSelectedTrackPosition(void)
|
||||
{
|
||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
||||
|
||||
for (Wt::WModelIndex index : indexSet)
|
||||
{
|
||||
if (!index.isValid())
|
||||
continue;
|
||||
|
||||
return index.row();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
TrackView::getTracks(std::vector<Track::id_type>& trackIds)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Getting all tracks...";
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
Wt::Dbo::Query<Track::UIQueryResult> query = _queryModel.query();
|
||||
Wt::Dbo::collection<Track::UIQueryResult> results = query.limit(-1).offset(-1);
|
||||
|
||||
for (auto it = results.begin(); it != results.end(); ++it)
|
||||
{
|
||||
Track::id_type id = it->get<0>();
|
||||
trackIds.push_back(id);
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Getting all tracks done! " << trackIds.size() << " tracks!";
|
||||
}
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,80 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TRACK_HPP
|
||||
#define TRACK_HPP
|
||||
|
||||
#include <Wt/WTableView>
|
||||
#include <Wt/Dbo/QueryModel>
|
||||
|
||||
#include "database/DatabaseHandler.hpp"
|
||||
|
||||
#include "Filter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Desktop {
|
||||
|
||||
class TrackView : public Wt::WTableView, public Filter
|
||||
{
|
||||
public:
|
||||
|
||||
TrackView(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
// Filter interface
|
||||
// Set constraints created by parent filters
|
||||
void refresh(Database::SearchFilter& filter);
|
||||
|
||||
// Create constraints for child filters (N/A)
|
||||
void getConstraint(Database::SearchFilter& filter) {}
|
||||
|
||||
// Get all the tracks that are currently selected
|
||||
void getSelectedTracks(std::vector<Database::Track::id_type>& 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<Database::Track::id_type>& track_ids);
|
||||
|
||||
typedef Wt::Signal<void> SigTrackDoubleClicked;
|
||||
typedef Wt::Signal<Wt::WString> SigStatsUpdated;
|
||||
|
||||
SigTrackDoubleClicked& trackDoubleClicked() { return _sigTrackDoubleClicked; }
|
||||
SigStatsUpdated& statsUpdated() { return _sigStatsUpdated; }
|
||||
|
||||
private:
|
||||
|
||||
SigTrackDoubleClicked _sigTrackDoubleClicked;
|
||||
SigStatsUpdated _sigStatsUpdated;
|
||||
|
||||
void emitStats(const Database::SearchFilter& filter);
|
||||
|
||||
typedef Database::Track::UIQueryResult ResultType;
|
||||
Wt::Dbo::QueryModel< ResultType > _queryModel;
|
||||
Wt::WTableView* _tableView;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Desktop
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WText>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WImage>
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ArtistSearch.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
ArtistSearch::ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
||||
t->setTemplateText(Wt::WString::tr("wa-artist-search"));
|
||||
|
||||
Wt::WTemplate *titleTemplate = new Wt::WTemplate(this);
|
||||
titleTemplate->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||
titleTemplate->bindString("text", title);
|
||||
t->bindWidget("title", titleTemplate);
|
||||
|
||||
_contents = new Wt::WContainerWidget();
|
||||
t->bindWidget("contents", _contents );
|
||||
|
||||
_showMore = new Wt::WTemplate();
|
||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||
_showMore->bindString("text", "Tap to show more results...");
|
||||
_showMore->hide();
|
||||
_showMore->clicked().connect(std::bind([=] {
|
||||
_sigShowMore.emit();
|
||||
}));
|
||||
t->bindWidget("show-more", _showMore);
|
||||
}
|
||||
|
||||
void
|
||||
ArtistSearch::clear()
|
||||
{
|
||||
_contents->clear();
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
void
|
||||
ArtistSearch::search(Database::SearchFilter filter, size_t nb)
|
||||
{
|
||||
_filter = filter;
|
||||
|
||||
clear();
|
||||
addResults(nb);
|
||||
}
|
||||
|
||||
void
|
||||
ArtistSearch::addResults(std::size_t nb)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
std::vector<Artist::pointer> artists = Artist::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
||||
|
||||
for (Artist::pointer artist : artists)
|
||||
{
|
||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
||||
res->setTemplateText(Wt::WString::tr("wa-artist-search-res"));
|
||||
|
||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt:: WLink::InternalPath, "/audio/artist/" + std::to_string(artist.id())));
|
||||
Wt::WImage *artistImg = new Wt::WImage(coverAnchor);
|
||||
artistImg->setImageLink( SessionImageResource()->getArtistUrl(artist.id(), 512));
|
||||
artistImg->setStyleClass("center-block"); // TODO move in css?
|
||||
artistImg->setStyleClass("release_res_shadow release_img-responsive"); // TODO move in css?
|
||||
|
||||
res->bindWidget("gif", coverAnchor);
|
||||
res->bindString("name", Wt::WString::fromUTF8(artist->getName(), Wt::PlainText));
|
||||
}
|
||||
|
||||
if (moreResults)
|
||||
_showMore->show();
|
||||
else
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_MOBILE_ARTIST_SEARCH_HPP
|
||||
#define UI_MOBILE_ARTIST_SEARCH_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/SearchFilter.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class ArtistSearch : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void search(Database::SearchFilter filter, std::size_t nb);
|
||||
void addResults(std::size_t nb);
|
||||
|
||||
// Slots
|
||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigShowMore;
|
||||
|
||||
void clear(void);
|
||||
|
||||
Wt::WTemplate* _showMore;
|
||||
|
||||
Database::SearchFilter _filter;
|
||||
Wt::WContainerWidget* _contents;
|
||||
std::size_t _count;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WApplication>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "SearchUtils.hpp"
|
||||
|
||||
#include "ArtistSearch.hpp"
|
||||
|
||||
#include "ArtistSearchView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
#define SEARCH_NB_ITEMS 20
|
||||
|
||||
using namespace Database;
|
||||
|
||||
ArtistSearchView::ArtistSearchView(Wt::WContainerWidget* parent)
|
||||
{
|
||||
ArtistSearch* artistSearch = new ArtistSearch("Artists", this);
|
||||
artistSearch->showMore().connect(std::bind([=] {
|
||||
artistSearch->addResults(SEARCH_NB_ITEMS);
|
||||
}));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/search/artist";
|
||||
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
||||
|
||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
|
||||
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 "LmsApplication.hpp"
|
||||
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "ReleaseSearch.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
ArtistView::ArtistView(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
ReleaseSearch *releases = new ReleaseSearch(this);
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/artist/";
|
||||
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::string strId = path.substr(pathPrefix.length());
|
||||
|
||||
Artist::id_type id;
|
||||
if (readAs(strId, id))
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
auto artist = Artist::getById(DboSession(), id);
|
||||
releases->search(SearchFilter::ById(SearchFilter::Field::Artist, id), 20, artist ? Wt::WString::fromUTF8(artist->getName()) : "Unknown artist");
|
||||
}
|
||||
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
ArtistView::getLink(Database::Artist::id_type id)
|
||||
{
|
||||
return Wt::WLink(Wt::WLink::InternalPath, "/audio/artist/" + std::to_string(id));
|
||||
}
|
||||
|
||||
} //namespace Mobile
|
||||
} //namespace UserInterface
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <map>
|
||||
|
||||
#include <Wt/WStackedWidget>
|
||||
#include <Wt/WTemplate>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "audio/AudioPlayer.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "PreviewSearchView.hpp"
|
||||
#include "ArtistSearchView.hpp"
|
||||
#include "ReleaseSearchView.hpp"
|
||||
#include "TrackSearchView.hpp"
|
||||
|
||||
#include "ArtistView.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
|
||||
#include "MobilePlayQueue.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "MobileAudio.hpp"
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
enum WidgetIdx
|
||||
{
|
||||
WidgetIdxSearchPreview = 0,
|
||||
WidgetIdxSearchArtist = 1,
|
||||
WidgetIdxSearchRelease = 2,
|
||||
WidgetIdxSearchTrack = 3,
|
||||
WidgetIdxArtist = 4,
|
||||
WidgetIdxRelease = 5,
|
||||
WidgetIdxPlayQueue = 6,
|
||||
};
|
||||
|
||||
using namespace Database;
|
||||
|
||||
void
|
||||
Audio::search(std::string text)
|
||||
{
|
||||
wApp->setInternalPath("/audio/search/preview/" + stringToUTF8(text), true);
|
||||
}
|
||||
|
||||
Audio::Audio(Wt::WContainerWidget *parent)
|
||||
: UserInterface::Audio(parent)
|
||||
{
|
||||
// Root div has to be a "container"
|
||||
this->setStyleClass("container-fluid");
|
||||
this->setPadding(60, Wt::Bottom);
|
||||
|
||||
Wt::WStackedWidget *stack = new Wt::WStackedWidget(this);
|
||||
|
||||
PlayQueue *playQueue = new PlayQueue();
|
||||
|
||||
// Same order as WidgetIdxXXX
|
||||
stack->addWidget(new PreviewSearchView(_playQueueEvents));
|
||||
stack->addWidget(new ArtistSearchView());
|
||||
stack->addWidget(new ReleaseSearchView());
|
||||
stack->addWidget(new TrackSearchView(_playQueueEvents));
|
||||
stack->addWidget(new ArtistView());
|
||||
stack->addWidget(new ReleaseView(_playQueueEvents));
|
||||
stack->addWidget(playQueue);
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
// order is important: most specific path first
|
||||
static std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/audio/search/preview", WidgetIdxSearchPreview},
|
||||
{ "/audio/search/artist", WidgetIdxSearchArtist},
|
||||
{ "/audio/search/release", WidgetIdxSearchRelease},
|
||||
{ "/audio/search/track", WidgetIdxSearchTrack},
|
||||
{ "/audio/artist", WidgetIdxArtist},
|
||||
{ "/audio/release", WidgetIdxRelease},
|
||||
{ "/audio/playqueue", WidgetIdxPlayQueue},
|
||||
};
|
||||
|
||||
for (auto index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
stack->setCurrentIndex(index.second);
|
||||
}
|
||||
|
||||
}, std::placeholders::_1));
|
||||
|
||||
Wt::WTemplate* footer = new Wt::WTemplate(this);
|
||||
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
|
||||
|
||||
AudioPlayer* audioPlayer = new AudioPlayer(AudioPlayer::ControlPlayqueue);
|
||||
footer->bindWidget("player", audioPlayer);
|
||||
|
||||
audioPlayer->showPlayQueue().connect(std::bind([=]
|
||||
{
|
||||
wApp->setInternalPath("/audio/playqueue", true);
|
||||
}));
|
||||
|
||||
// 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
|
||||
_playQueueEvents.trackPlay.connect(std::bind([=] (Database::Track::id_type id)
|
||||
{
|
||||
std::size_t pos = playQueue->addTrack(id);
|
||||
playQueue->play(pos);
|
||||
}, std::placeholders::_1));
|
||||
|
||||
_playQueueEvents.trackAdd.connect(std::bind([=] (Database::Track::id_type id)
|
||||
{
|
||||
playQueue->addTrack(id);
|
||||
}, std::placeholders::_1));
|
||||
|
||||
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* 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 "utils/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
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WApplication>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "SearchUtils.hpp"
|
||||
|
||||
#include "ArtistSearch.hpp"
|
||||
#include "ReleaseSearch.hpp"
|
||||
#include "TrackSearch.hpp"
|
||||
|
||||
#include "PreviewSearchView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
#define SEARCH_NB_ITEMS 4
|
||||
|
||||
using namespace Database;
|
||||
|
||||
PreviewSearchView::PreviewSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/search/preview";
|
||||
|
||||
ArtistSearch* artistSearch = new ArtistSearch("Artists", this);
|
||||
ReleaseSearch* releaseSearch = new ReleaseSearch(this);
|
||||
TrackSearch* trackSearch = new TrackSearch(events, this);
|
||||
|
||||
artistSearch->showMore().connect(std::bind([=]
|
||||
{
|
||||
std::string path = wApp->internalPath();
|
||||
path.replace(0, pathPrefix.length(), "/audio/search/artist");
|
||||
|
||||
wApp->setInternalPath(path, true);
|
||||
}));
|
||||
|
||||
releaseSearch->showMore().connect(std::bind([=]
|
||||
{
|
||||
std::string path = wApp->internalPath();
|
||||
path.replace(0, pathPrefix.length(), "/audio/search/release");
|
||||
|
||||
wApp->setInternalPath(path, true);
|
||||
}));
|
||||
|
||||
trackSearch->showMore().connect(std::bind([=]
|
||||
{
|
||||
std::string path = wApp->internalPath();
|
||||
path.replace(0, pathPrefix.length(), "/audio/search/track");
|
||||
|
||||
wApp->setInternalPath(path, true);
|
||||
}));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
||||
|
||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
|
||||
releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS, "Releases");
|
||||
trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
|
||||
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WText>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WTemplate>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ReleaseSearch.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
ReleaseSearch::ReleaseSearch(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
||||
t->setTemplateText(Wt::WString::tr("wa-release-search"));
|
||||
|
||||
Wt::WTemplate *titleTemplate = new Wt::WTemplate(this);
|
||||
titleTemplate->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||
|
||||
_title = new Wt::WText();
|
||||
titleTemplate->bindWidget("text", _title);
|
||||
|
||||
t->bindWidget("title", titleTemplate);
|
||||
|
||||
_contents = new Wt::WContainerWidget();
|
||||
t->bindWidget("contents", _contents );
|
||||
|
||||
_showMore = new Wt::WTemplate();
|
||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||
_showMore->bindString("text", "Tap to show more results...");
|
||||
_showMore->hide();
|
||||
_showMore->clicked().connect(std::bind([=] {
|
||||
_sigShowMore.emit();
|
||||
}));
|
||||
|
||||
t->bindWidget("show-more", _showMore);
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseSearch::clear()
|
||||
{
|
||||
_contents->clear();
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseSearch::search(Database::SearchFilter filter, size_t max, Wt::WString title)
|
||||
{
|
||||
_filter = filter;
|
||||
_title->setText(title);
|
||||
|
||||
clear();
|
||||
addResults(max);
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseSearch::addResults(size_t nb)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
std::vector<Release::pointer> releases = Release::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
||||
|
||||
for (Release::pointer release : releases)
|
||||
{
|
||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
||||
res->setTemplateText(Wt::WString::tr("wa-release-search-res"));
|
||||
|
||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(ReleaseView::getLink(release.id()));
|
||||
Wt::WImage *cover = new Wt::WImage(coverAnchor);
|
||||
cover->setStyleClass("center-block");
|
||||
cover->setImageLink( SessionImageResource()->getReleaseUrl(release.id(), 512));
|
||||
cover->setStyleClass("release_res_shadow release_img-responsive"); // TODO move?
|
||||
|
||||
res->bindWidget("cover", coverAnchor);
|
||||
res->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText));
|
||||
res->bindString("release_name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
||||
|
||||
bool variousArtists;
|
||||
auto artists = Artist::getByFilter(DboSession(),
|
||||
SearchFilter::ById(SearchFilter::Field::Release, release.id()), 0, 1, variousArtists);
|
||||
if (!artists.empty())
|
||||
{
|
||||
if (variousArtists)
|
||||
{
|
||||
res->bindWidget("artist", new Wt::WText(Wt::WString::fromUTF8("Various Artists", Wt::PlainText)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(ArtistView::getLink(artists.front().id()));
|
||||
Wt::WText *artist = new Wt::WText(artistAnchor);
|
||||
artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText));
|
||||
res->bindWidget("artist", artistAnchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moreResults)
|
||||
_showMore->show();
|
||||
else
|
||||
_showMore->hide();
|
||||
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <Wt/WContainerWidget>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "database/SearchFilter.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class ReleaseSearch : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
ReleaseSearch(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void search(Database::SearchFilter filter, std::size_t nb, Wt::WString title);
|
||||
void addResults(std::size_t nb);
|
||||
|
||||
// Slots
|
||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigShowMore;
|
||||
|
||||
void clear(void);
|
||||
|
||||
Wt::WTemplate* _showMore;
|
||||
Database::SearchFilter _filter;
|
||||
Wt::WContainerWidget* _contents;
|
||||
Wt::WText* _title;
|
||||
std::size_t _count;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WApplication>
|
||||
|
||||
#include "SearchUtils.hpp"
|
||||
|
||||
#include "ReleaseSearch.hpp"
|
||||
|
||||
#include "ReleaseSearchView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
#define SEARCH_NB_ITEMS 20
|
||||
|
||||
using namespace Database;
|
||||
|
||||
ReleaseSearchView::ReleaseSearchView(Wt::WContainerWidget* parent)
|
||||
{
|
||||
ReleaseSearch* artistSearch = new ReleaseSearch(this);
|
||||
artistSearch->showMore().connect(std::bind([=] {
|
||||
artistSearch->addResults(SEARCH_NB_ITEMS);
|
||||
}));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/search/release";
|
||||
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
||||
|
||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS, "Releases");
|
||||
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WText>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WTemplate>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ReleaseView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
ReleaseView::ReleaseView(PlayQueueEvents& events, Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent),
|
||||
_events(events)
|
||||
{
|
||||
Wt::WTemplate* wrapper = new Wt::WTemplate(this);
|
||||
wrapper->setTemplateText(Wt::WString::tr("wa-trackview-wrapper"));
|
||||
|
||||
Wt::WTemplate* title = new Wt::WTemplate(this);
|
||||
wrapper->bindWidget("title", title);
|
||||
|
||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||
title->bindString("text", "Releases", Wt::PlainText);
|
||||
|
||||
_releaseContainer = new Wt::WContainerWidget();
|
||||
wrapper->bindWidget("release-container", _releaseContainer);
|
||||
|
||||
_showMore = new Wt::WTemplate();
|
||||
wrapper->bindWidget("show-more", _showMore);
|
||||
|
||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||
_showMore->bindString("text", "Tap to show more results...");
|
||||
_showMore->hide();
|
||||
_showMore->clicked().connect(std::bind([=] {
|
||||
addResults(20);
|
||||
}));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/release/";
|
||||
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::string strId = path.substr(pathPrefix.length());
|
||||
|
||||
Release::id_type id;
|
||||
if (readAs(strId, id))
|
||||
{
|
||||
clear();
|
||||
search(SearchFilter::ById(SearchFilter::Field::Release, id), 20);
|
||||
}
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseView::clear()
|
||||
{
|
||||
// Flush the release container
|
||||
_releaseContainer->clear();
|
||||
|
||||
// Flush the current context
|
||||
_currentTrackContainer = nullptr;
|
||||
_nbTracks = 0;
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseView::search(SearchFilter filter, size_t nb)
|
||||
{
|
||||
_filter = filter;
|
||||
|
||||
clear();
|
||||
addResults(nb);
|
||||
}
|
||||
|
||||
static Wt::WString
|
||||
getArtistNameFromRelease(Release::pointer release)
|
||||
{
|
||||
auto artists = Artist::getByFilter(DboSession(),
|
||||
SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
|
||||
|
||||
if (artists.size() > 1)
|
||||
return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
|
||||
else if (artists.size() == 1)
|
||||
return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
|
||||
else
|
||||
return Wt::WString();
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseView::addResults(size_t nb)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
std::vector<Track::pointer> tracks = Track::getByFilter(DboSession(), _filter, _nbTracks, nb, moreResults);
|
||||
|
||||
for (Track::pointer track : tracks)
|
||||
{
|
||||
// First check if we need to create a new track container
|
||||
|
||||
// New container if it is the first one or if the release has changed
|
||||
if (!_currentTrackContainer
|
||||
|| _currentReleaseId != track->getRelease().id())
|
||||
{
|
||||
Release::pointer release = track->getRelease();
|
||||
|
||||
Wt::WTemplate *releaseContainer = new Wt::WTemplate(_releaseContainer);
|
||||
releaseContainer->setTemplateText(Wt::WString::tr("wa-trackview-release-container"));
|
||||
_currentReleaseId = release.id();
|
||||
|
||||
Wt::WImage *cover = new Wt::WImage();
|
||||
cover->setStyleClass ("center-block img-responsive"); // TODO move to CSS?
|
||||
cover->setImageLink(Wt::WLink (SessionImageResource()->getReleaseUrl(release.id(), 512)));
|
||||
|
||||
releaseContainer->bindWidget("cover", cover);
|
||||
releaseContainer->bindString("artist-name", getArtistNameFromRelease(release), Wt::PlainText);
|
||||
releaseContainer->bindString("release-name", release->getName(), Wt::PlainText);
|
||||
int year = release->getReleaseYear();
|
||||
if (year > 0)
|
||||
{
|
||||
releaseContainer->setCondition("if-has-year", true);
|
||||
releaseContainer->bindInt("year", year);
|
||||
|
||||
int originalYear = release->getReleaseYear(true);
|
||||
if (originalYear > 0 && originalYear != year)
|
||||
{
|
||||
releaseContainer->setCondition("if-has-orig-year", true);
|
||||
releaseContainer->bindInt("orig-year", originalYear);
|
||||
}
|
||||
}
|
||||
|
||||
_currentTrackContainer = new Wt::WContainerWidget();
|
||||
releaseContainer->bindWidget("track-container", _currentTrackContainer);
|
||||
}
|
||||
|
||||
assert(_currentTrackContainer != nullptr);
|
||||
|
||||
Wt::WTemplate* trackRes = new Wt::WTemplate(_currentTrackContainer);
|
||||
trackRes->setTemplateText(Wt::WString::tr("wa-trackview-track"));
|
||||
|
||||
if (track->getTrackNumber() > 0 && !track->getRelease()->isNone())
|
||||
{
|
||||
trackRes->setCondition("if-has-track-num", true);
|
||||
trackRes->bindInt("track-num", track->getTrackNumber());
|
||||
|
||||
if (track->getDiscNumber() > 0 && track->getTotalDiscNumber() > 1)
|
||||
{ trackRes->setCondition("if-has-disc-num", true);
|
||||
trackRes->bindInt("disc-num", track->getDiscNumber());
|
||||
}
|
||||
}
|
||||
trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||
// TODO, display artist name for compilation releases?
|
||||
|
||||
Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
||||
trackRes->bindWidget("play-btn", playBtn);
|
||||
playBtn->setStyleClass("mobile-btn");
|
||||
playBtn->clicked().connect(std::bind([=] {
|
||||
_events.trackPlay.emit(track.id());
|
||||
}));
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
if (moreResults)
|
||||
_showMore->show();
|
||||
else
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
ReleaseView::getLink(Database::Release::id_type id)
|
||||
{
|
||||
return Wt::WLink(Wt::WLink::InternalPath, "/audio/release/" + std::to_string(id));
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <Wt/WContainerWidget>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "MobilePlayQueueEvents.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class ReleaseView : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
static Wt::WLink getLink(Database::Release::id_type id);
|
||||
|
||||
ReleaseView(PlayQueueEvents& events, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void search(Database::SearchFilter filter, size_t nb);
|
||||
|
||||
// Slots
|
||||
Wt::Signal<void>& moreTracksSelected() { return _sigMoreTracksSelected;}
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigMoreTracksSelected;
|
||||
|
||||
void clear(void);
|
||||
void addResults(size_t nb);
|
||||
|
||||
PlayQueueEvents& _events;
|
||||
Wt::WTemplate* _showMore;
|
||||
|
||||
Database::SearchFilter _filter;
|
||||
std::size_t _nbTracks;
|
||||
|
||||
// Main container that holds the releases
|
||||
Wt::WContainerWidget* _releaseContainer;
|
||||
|
||||
// Used to add more results on the fly
|
||||
Database::Release::id_type _currentReleaseId;
|
||||
Wt::WContainerWidget* _currentTrackContainer;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 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 "utils/Utils.hpp"
|
||||
|
||||
static inline std::vector<std::string> searchPathToSearchKeywords(std::string path)
|
||||
{
|
||||
if (!path.empty() && path[0] == '/')
|
||||
path.erase(path.begin());
|
||||
|
||||
return splitString(path, " \t");
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/WText>
|
||||
#include <Wt/WImage>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WTemplate>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "TrackSearch.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TrackSearch::TrackSearch(PlayQueueEvents& events, Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent),
|
||||
_events(events)
|
||||
{
|
||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
||||
t->setTemplateText(Wt::WString::tr("wa-track-search"));
|
||||
|
||||
Wt::WTemplate* title = new Wt::WTemplate(this);
|
||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||
title->bindString("text", "Tracks", Wt::PlainText);
|
||||
t->bindWidget("title", title);
|
||||
|
||||
_contents = new Wt::WContainerWidget();
|
||||
t->bindWidget("contents", _contents);
|
||||
|
||||
_showMore = new Wt::WTemplate();
|
||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||
_showMore->bindString("text", "Tap to show more results...");
|
||||
_showMore->hide();
|
||||
_showMore->clicked().connect(std::bind([=] {
|
||||
_sigShowMore.emit();
|
||||
addResults(20);
|
||||
}));
|
||||
t->bindWidget("show-more", _showMore);
|
||||
}
|
||||
|
||||
void
|
||||
TrackSearch::clear()
|
||||
{
|
||||
_contents->clear();
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
void
|
||||
TrackSearch::search(SearchFilter filter, size_t nb)
|
||||
{
|
||||
_filter = filter;
|
||||
|
||||
clear();
|
||||
addResults(nb);
|
||||
}
|
||||
|
||||
void
|
||||
TrackSearch::addResults(size_t nb)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
bool moreResults;
|
||||
std::vector<Track::pointer > tracks = Track::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
||||
|
||||
for (Track::pointer track : tracks)
|
||||
{
|
||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
||||
res->setTemplateText(Wt::WString::tr("wa-track-search-res"));
|
||||
|
||||
Wt::WImage *cover = new Wt::WImage();
|
||||
res->bindWidget("cover", cover);
|
||||
cover->setStyleClass ("center-block img-responsive");
|
||||
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
|
||||
res->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||
res->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);
|
||||
res->bindWidget("play-btn", playBtn);
|
||||
playBtn->setStyleClass("mobile-btn");
|
||||
playBtn->clicked().connect(std::bind([=] {
|
||||
_events.trackPlay.emit(track.id());
|
||||
}));
|
||||
|
||||
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)
|
||||
_showMore->show();
|
||||
else
|
||||
_showMore->hide();
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <Wt/WContainerWidget>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "database/SearchFilter.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "MobilePlayQueueEvents.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
class TrackSearch : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
TrackSearch(PlayQueueEvents& events, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void addResults(size_t nb);
|
||||
void search(Database::SearchFilter filter, size_t nb);
|
||||
|
||||
// Slots
|
||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigShowMore;
|
||||
|
||||
void clear(void);
|
||||
|
||||
PlayQueueEvents& _events;
|
||||
Wt::WTemplate* _showMore;
|
||||
Database::SearchFilter _filter;
|
||||
Wt::WContainerWidget* _contents;
|
||||
};
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* 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/WApplication>
|
||||
#include <Wt/WText>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "SearchUtils.hpp"
|
||||
|
||||
#include "TrackSearch.hpp"
|
||||
|
||||
#include "TrackSearchView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Mobile {
|
||||
|
||||
#define SEARCH_NB_ITEMS 20
|
||||
|
||||
using namespace Database;
|
||||
|
||||
TrackSearchView::TrackSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent)
|
||||
{
|
||||
TrackSearch* trackSearch = new TrackSearch(events, this);
|
||||
trackSearch->showMore().connect(std::bind([=] {
|
||||
trackSearch->addResults(SEARCH_NB_ITEMS);
|
||||
}));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
||||
{
|
||||
const std::string pathPrefix = "/audio/search/track";
|
||||
|
||||
if (!wApp->internalPathMatches(pathPrefix))
|
||||
return;
|
||||
|
||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
||||
|
||||
trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
|
||||
|
||||
}, std::placeholders::_1));
|
||||
}
|
||||
|
||||
} // namespace Mobile
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,110 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WMenu>
|
||||
#include <Wt/WStackedWidget>
|
||||
#include <Wt/WTextArea>
|
||||
#include <Wt/WHBoxLayout>
|
||||
|
||||
#include "SettingsAudioFormView.hpp"
|
||||
#include "SettingsUserFormView.hpp"
|
||||
#include "SettingsAccountFormView.hpp"
|
||||
#include "SettingsDatabaseFormView.hpp"
|
||||
#include "SettingsMediaDirectories.hpp"
|
||||
#include "SettingsUsers.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Setting.hpp"
|
||||
#include "database/updater/DatabaseUpdater.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "Settings.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
Settings::Settings(Wt::WContainerWidget* parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
Wt::WHBoxLayout* hLayout = new Wt::WHBoxLayout(this);
|
||||
|
||||
// Create a stack where the contents will be located.
|
||||
Wt::WStackedWidget *contents = new Wt::WStackedWidget();
|
||||
|
||||
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);
|
||||
menu->setInternalPathEnabled();
|
||||
menu->setInternalBasePath("/settings/");
|
||||
|
||||
hLayout->addWidget(menu);
|
||||
hLayout->addWidget(contents, 1);
|
||||
|
||||
std::string userId;
|
||||
bool userIsAdmin;
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
userId = User::getId(CurrentUser());
|
||||
userIsAdmin = CurrentUser()->isAdmin();
|
||||
}
|
||||
|
||||
menu->addItem("Audio", new AudioFormView())->setPathComponent("audio");
|
||||
if (userIsAdmin)
|
||||
{
|
||||
MediaDirectories* mediaDirectories = new MediaDirectories();
|
||||
mediaDirectories->changed().connect(std::bind([=]
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Media directories have changed: requesting imediate scan";
|
||||
|
||||
// On directory change, request an immediate scan
|
||||
std::lock_guard<std::mutex> lock(Updater::instance().getMutex());
|
||||
|
||||
Setting::setBool(DboSession(), "manual_scan_requested", true);
|
||||
Updater::instance().restart();
|
||||
}));
|
||||
menu->addItem("Media Folders", mediaDirectories)->setPathComponent("mediadirectories");
|
||||
|
||||
DatabaseFormView* databaseFormView = new DatabaseFormView();
|
||||
databaseFormView->changed().connect(std::bind([=]
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(Updater::instance().getMutex());
|
||||
Updater::instance().restart();
|
||||
}));
|
||||
menu->addItem("Database", databaseFormView)->setPathComponent("database");
|
||||
|
||||
menu->addItem("Users", new Users())->setPathComponent("users");
|
||||
}
|
||||
else
|
||||
{
|
||||
menu->addItem("Account", new AccountFormView(userId))->setPathComponent("account");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,285 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WCheckBox>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/Auth/Identity>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsAccountFormView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class AccountFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field NameField;
|
||||
static const Field EmailField;
|
||||
static const Field PasswordField;
|
||||
static const Field PasswordConfirmField;
|
||||
|
||||
AccountFormModel(std::string userId, Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent),
|
||||
_userId(userId)
|
||||
{
|
||||
addField(NameField);
|
||||
addField(EmailField);
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
|
||||
setValidator(NameField, createNameValidator());
|
||||
setValidator(EmailField, createEmailValidator());
|
||||
|
||||
// populate the model with initial data
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData()
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( _userId );
|
||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
||||
|
||||
if (user && authUser.isValid())
|
||||
{
|
||||
setValue(NameField, authUser.identity(Wt::Auth::Identity::LoginName));
|
||||
if (!authUser.email().empty())
|
||||
setValue(EmailField, authUser.email());
|
||||
else
|
||||
setValue(EmailField, authUser.unverifiedEmail());
|
||||
}
|
||||
setValue(PasswordField, Wt::WString());
|
||||
setValue(PasswordConfirmField, Wt::WString());
|
||||
}
|
||||
|
||||
bool saveData(Wt::WString& error)
|
||||
{
|
||||
// DBO transaction active here
|
||||
try {
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Update user
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId(_userId);
|
||||
Database::User::pointer user = DbHandler().getUser( authUser );
|
||||
|
||||
// user may have been deleted by someone else
|
||||
if (!authUser.isValid()) {
|
||||
error = Wt::WString("User identity does not exist");
|
||||
return false;
|
||||
}
|
||||
else if(!user)
|
||||
{
|
||||
error = Wt::WString("User not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Account
|
||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
||||
|
||||
// Password
|
||||
if (!valueText(PasswordField).empty())
|
||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
||||
|
||||
setValue(PasswordField, Wt::WString());
|
||||
setValue(PasswordConfirmField, Wt::WString());
|
||||
|
||||
}
|
||||
catch(Wt::Dbo::Exception& exception)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
// DBO transaction active here
|
||||
|
||||
Wt::WString error;
|
||||
|
||||
if (field == NameField)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
// Must be unique since used as LoginIdentity
|
||||
Wt::Auth::User user = DbHandler().getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, valueText(field));
|
||||
if (user.isValid() && user.id() != _userId)
|
||||
error = "Already exists";
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordField)
|
||||
{
|
||||
// Password is mandatory if we create the user
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
||||
valueText(NameField),
|
||||
valueText(EmailField).toUTF8());
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordConfirmField)
|
||||
{
|
||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
||||
{
|
||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply validators
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
|
||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
||||
|
||||
return validation(field).state() == Wt::WValidator::Valid;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
|
||||
std::string _userId;
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field AccountFormModel::NameField = "name";
|
||||
const Wt::WFormModel::Field AccountFormModel::EmailField = "email";
|
||||
const Wt::WFormModel::Field AccountFormModel::PasswordField = "password";
|
||||
const Wt::WFormModel::Field AccountFormModel::PasswordConfirmField = "password-confirm";
|
||||
|
||||
AccountFormView::AccountFormView(std::string userId, Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
|
||||
_model = new AccountFormModel(userId, this);
|
||||
|
||||
setTemplateText(tr("settings-account"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
_applyInfo = new Wt::WText();
|
||||
_applyInfo->setInline(false);
|
||||
_applyInfo->hide();
|
||||
bindWidget("apply-info", _applyInfo);
|
||||
|
||||
// Name
|
||||
Wt::WLineEdit* accountEdit = new Wt::WLineEdit();
|
||||
setFormWidget(AccountFormModel::NameField, accountEdit);
|
||||
accountEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Email
|
||||
Wt::WLineEdit* emailEdit = new Wt::WLineEdit();
|
||||
setFormWidget(AccountFormModel::EmailField, emailEdit);
|
||||
emailEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Password
|
||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
||||
setFormWidget(AccountFormModel::PasswordField, passwordEdit );
|
||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
passwordEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Password confirmation
|
||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
||||
setFormWidget(AccountFormModel::PasswordConfirmField, passwordConfirmEdit);
|
||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
passwordConfirmEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Title & Buttons
|
||||
bindString("title", "Account settings");
|
||||
|
||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
||||
saveButton->setStyleClass("btn-primary");
|
||||
bindWidget("save-button", saveButton);
|
||||
saveButton->clicked().connect(this, &AccountFormView::processSave);
|
||||
|
||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Discard");
|
||||
bindWidget("cancel-button", cancelButton);
|
||||
cancelButton->clicked().connect(this, &AccountFormView::processCancel);
|
||||
|
||||
updateView(_model);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
AccountFormView::processCancel()
|
||||
{
|
||||
_applyInfo->show();
|
||||
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
||||
_applyInfo->setStyleClass("alert alert-info");
|
||||
_model->loadData();
|
||||
|
||||
_model->validate();
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
void
|
||||
AccountFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
_applyInfo->show();
|
||||
if (_model->validate())
|
||||
{
|
||||
Wt::WString error;
|
||||
// commit model into DB
|
||||
if (_model->saveData(error) )
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
||||
_applyInfo->setStyleClass("alert alert-success");
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,49 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_ACCOUNT_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_ACCOUNT_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WText>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class AccountFormModel;
|
||||
|
||||
class AccountFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
AccountFormView(std::string userId, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
private:
|
||||
void processCancel(); // reload from DB
|
||||
void processSave(); // commit into DB
|
||||
|
||||
AccountFormModel* _model;
|
||||
Wt::WText* _applyInfo;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,258 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WStringListModel>
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsAudioFormView.hpp"
|
||||
|
||||
namespace {
|
||||
std::string encodingToString(Database::AudioEncoding encoding)
|
||||
{
|
||||
|
||||
switch (encoding)
|
||||
{
|
||||
case Database::AudioEncoding::AUTO: return "Automatic";
|
||||
case Database::AudioEncoding::MP3: return "MP3";
|
||||
case Database::AudioEncoding::OGA: return "OGG";
|
||||
case Database::AudioEncoding::WEBMA: return "WebM";
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class AudioFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field BitrateField;
|
||||
static const Field EncodingField;
|
||||
|
||||
AudioFormModel(Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent)
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
addField(BitrateField);
|
||||
addField(EncodingField);
|
||||
|
||||
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
||||
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
||||
|
||||
// populate the model with initial data
|
||||
loadData();
|
||||
}
|
||||
|
||||
Wt::WAbstractItemModel *bitrateModel() { return _bitrateModel; }
|
||||
Wt::WAbstractItemModel *encodingModel() { return _encodingModel; }
|
||||
|
||||
void loadData()
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
setValue(BitrateField, std::min(CurrentUser()->getMaxAudioBitrate(), CurrentUser()->getAudioBitrate()) / 1000); // in kps
|
||||
int encodingRow = getAudioEncodingRow( CurrentUser()->getAudioEncoding());
|
||||
setValue(EncodingField, getAudioEncodingString(encodingRow));
|
||||
}
|
||||
|
||||
bool saveData(Wt::WString& error)
|
||||
{
|
||||
// DBO transaction active here
|
||||
try {
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// user may have been deleted by someone else
|
||||
CurrentUser().modify()->setAudioBitrate( Wt::asNumber(value(BitrateField)) * 1000); // in kbps
|
||||
|
||||
int encodingRow = getAudioEncodingRow( boost::any_cast<Wt::WString>(value((EncodingField))));
|
||||
CurrentUser().modify()->setAudioEncoding( getAudioEncodingValue(encodingRow));
|
||||
}
|
||||
catch(Wt::Dbo::Exception& exception)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int getAudioEncodingRow( Database::AudioEncoding encoding)
|
||||
{
|
||||
for (int i = 0; i < _encodingModel->rowCount(); ++i)
|
||||
{
|
||||
if (getAudioEncodingValue(i) == encoding)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int getAudioEncodingRow( Wt::WString encoding)
|
||||
{
|
||||
for (int i = 0; i < _encodingModel->rowCount(); ++i)
|
||||
{
|
||||
if (getAudioEncodingString(i) == encoding)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Database::AudioEncoding getAudioEncodingValue(int row)
|
||||
{
|
||||
return boost::any_cast<Database::AudioEncoding>
|
||||
(_encodingModel->data(_encodingModel->index(row, 0), Wt::UserRole));
|
||||
}
|
||||
|
||||
Wt::WString getAudioEncodingString(int row)
|
||||
{
|
||||
return boost::any_cast<Wt::WString>
|
||||
(_encodingModel->data(_encodingModel->index(row, 0), Wt::DisplayRole));
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
void initializeModels()
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
_bitrateModel = new Wt::WStringListModel(this);
|
||||
BOOST_FOREACH(std::size_t bitrate, Database::User::audioBitrates)
|
||||
{
|
||||
if (bitrate <= CurrentUser()->getMaxAudioBitrate())
|
||||
_bitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps
|
||||
}
|
||||
|
||||
_encodingModel = new Wt::WStringListModel(this);
|
||||
int row = 0;
|
||||
BOOST_FOREACH(Database::AudioEncoding encoding, Database::User::audioEncodings)
|
||||
{
|
||||
_encodingModel->addString(encodingToString(encoding));
|
||||
_encodingModel->setData(row, 0, encoding, Wt::UserRole);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
std::string _userId;
|
||||
Wt::WStringListModel* _bitrateModel;
|
||||
Wt::WStringListModel* _encodingModel;
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field AudioFormModel::BitrateField = "bitrate";
|
||||
const Wt::WFormModel::Field AudioFormModel::EncodingField = "encoding";
|
||||
|
||||
AudioFormView::AudioFormView(Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
|
||||
_model = new AudioFormModel(this);
|
||||
|
||||
setTemplateText(tr("settings-audio"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
_applyInfo = new Wt::WText();
|
||||
_applyInfo->setInline(false);
|
||||
_applyInfo->hide();
|
||||
bindWidget("apply-info", _applyInfo);
|
||||
|
||||
// Bitrate
|
||||
Wt::WComboBox *bitrateCB = new Wt::WComboBox();
|
||||
setFormWidget(AudioFormModel::BitrateField, bitrateCB);
|
||||
bitrateCB->setStyleClass("span2");
|
||||
bitrateCB->setModel(_model->bitrateModel());
|
||||
bitrateCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Encoding
|
||||
Wt::WComboBox *encodingCB = new Wt::WComboBox();
|
||||
setFormWidget(AudioFormModel::EncodingField, encodingCB);
|
||||
encodingCB->setModel(_model->encodingModel());
|
||||
encodingCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Title & Buttons
|
||||
bindString("title", "Audio settings");
|
||||
|
||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
||||
saveButton->setStyleClass("btn-primary");
|
||||
bindWidget("save-button", saveButton);
|
||||
saveButton->clicked().connect(this, &AudioFormView::processSave);
|
||||
|
||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Discard");
|
||||
bindWidget("cancel-button", cancelButton);
|
||||
cancelButton->clicked().connect(this, &AudioFormView::processCancel);
|
||||
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
void
|
||||
AudioFormView::processCancel()
|
||||
{
|
||||
_applyInfo->show();
|
||||
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
||||
_applyInfo->setStyleClass("alert alert-info");
|
||||
_model->loadData();
|
||||
|
||||
_model->validate();
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
void
|
||||
AudioFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
_applyInfo->show();
|
||||
if (_model->validate())
|
||||
{
|
||||
Wt::WString error;
|
||||
// commit model into DB
|
||||
if (_model->saveData(error) )
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
||||
_applyInfo->setStyleClass("alert alert-success");
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,49 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_AUDIO_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_AUDIO_ACCOUNT_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WText>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class AudioFormModel;
|
||||
|
||||
class AudioFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
AudioFormView(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
private:
|
||||
void processCancel(); // reload from DB
|
||||
void processSave(); // commit into DB
|
||||
|
||||
AudioFormModel* _model;
|
||||
Wt::WText* _applyInfo;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,400 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WString>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WCheckBox>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WMessageBox>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WSpinBox>
|
||||
#include <Wt/WRegExpValidator>
|
||||
#include <Wt/WIntValidator>
|
||||
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WStringListModel>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Setting.hpp"
|
||||
|
||||
#include "common/DirectoryValidator.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsDatabaseFormView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
class DatabaseFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field UpdatePeriodField;
|
||||
static const Field UpdateStartTimeField;
|
||||
static const Field AudioFileExtensionsField;
|
||||
static const Field VideoFileExtensionsField;
|
||||
static const Field TagsHighLevelAcousticBrainz;
|
||||
static const Field TagsHighLevelAcousticBrainzMinProbability;
|
||||
static const Field TagsSimilarityAcousticBrainz;
|
||||
|
||||
DatabaseFormModel(Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent)
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
addField(UpdatePeriodField);
|
||||
addField(UpdateStartTimeField);
|
||||
addField(AudioFileExtensionsField);
|
||||
addField(VideoFileExtensionsField);
|
||||
addField(TagsHighLevelAcousticBrainz);
|
||||
addField(TagsHighLevelAcousticBrainzMinProbability);
|
||||
addField(TagsSimilarityAcousticBrainz);
|
||||
|
||||
setValidator(UpdatePeriodField, createUpdatePeriodValidator());
|
||||
setValidator(UpdateStartTimeField, createStartTimeValidator());
|
||||
setValidator(AudioFileExtensionsField, createFileExtensionValidator());
|
||||
setValidator(VideoFileExtensionsField, createFileExtensionValidator());
|
||||
setValidator(TagsHighLevelAcousticBrainzMinProbability, createMinProbabilityValidator());
|
||||
|
||||
// populate the model with initial data
|
||||
loadData();
|
||||
}
|
||||
|
||||
Wt::WAbstractItemModel *updatePeriodModel() { return _updatePeriodModel; }
|
||||
Wt::WAbstractItemModel *updateStartTimeModel() { return _updateStartTimeModel; }
|
||||
|
||||
void loadData()
|
||||
{
|
||||
int periodRow = getUpdatePeriodModelRow( Setting::getString(DboSession(), "update_period"));
|
||||
if (periodRow != -1)
|
||||
setValue(UpdatePeriodField, updatePeriodDisplay(periodRow));
|
||||
|
||||
int startTimeRow = getUpdateStartTimeModelRow( Setting::getDuration(DboSession(), "update_start_time") );
|
||||
if (startTimeRow != -1)
|
||||
setValue(UpdateStartTimeField, updateStartTime( startTimeRow ) );
|
||||
|
||||
setValue(AudioFileExtensionsField, Setting::getString(DboSession(), "audio_file_extensions"));
|
||||
setValue(VideoFileExtensionsField, Setting::getString(DboSession(), "video_file_extensions"));
|
||||
|
||||
setValue(TagsHighLevelAcousticBrainz, Setting::getBool(DboSession(), "tags_highlevel_acousticbrainz"));
|
||||
setValue(TagsHighLevelAcousticBrainzMinProbability, Setting::getInt(DboSession(), "tags_highlevel_acousticbrainz_min_probability"));
|
||||
setValue(TagsSimilarityAcousticBrainz, Setting::getBool(DboSession(), "tags_similarity_acousticbrainz"));
|
||||
}
|
||||
|
||||
void saveData()
|
||||
{
|
||||
int periodRow = getUpdatePeriodModelRow( boost::any_cast<Wt::WString>(value(UpdatePeriodField)));
|
||||
assert(periodRow != -1);
|
||||
Setting::setString(DboSession(), "update_period", updatePeriodSetting( periodRow ) );
|
||||
|
||||
int startTimeRow = getUpdateStartTimeModelRow( boost::any_cast<Wt::WString>(value(UpdateStartTimeField)));
|
||||
assert(startTimeRow != -1);
|
||||
Setting::setDuration(DboSession(), "update_start_time", updateStartTimeDuration( startTimeRow ) );
|
||||
|
||||
Setting::setString(DboSession(), "audio_file_extensions", boost::any_cast<Wt::WString>(value(AudioFileExtensionsField)).toUTF8());
|
||||
Setting::setString(DboSession(), "video_file_extensions", boost::any_cast<Wt::WString>(value(VideoFileExtensionsField)).toUTF8());
|
||||
|
||||
Setting::setBool(DboSession(), "tags_highlevel_acousticbrainz", boost::any_cast<bool>(value(TagsHighLevelAcousticBrainz)));
|
||||
Setting::setInt(DboSession(), "tags_highlevel_acousticbrainz_min_probability", std::stoi(boost::any_cast<Wt::WString>(value(TagsHighLevelAcousticBrainzMinProbability)).toUTF8()));
|
||||
Setting::setBool(DboSession(), "tags_similarity_acousticbrainz", boost::any_cast<bool>(value(TagsSimilarityAcousticBrainz)));
|
||||
}
|
||||
|
||||
void setImmediateScan()
|
||||
{
|
||||
Setting::setBool(DboSession(), "manual_scan_requested", true);
|
||||
}
|
||||
|
||||
int getUpdatePeriodModelRow(Wt::WString value)
|
||||
{
|
||||
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
|
||||
{
|
||||
if (updatePeriodDisplay(i) == value)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int getUpdatePeriodModelRow(std::string value)
|
||||
{
|
||||
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
|
||||
{
|
||||
if (updatePeriodSetting(i) == value)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string updatePeriodSetting(int row) {
|
||||
return boost::any_cast<std::string>
|
||||
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::UserRole));
|
||||
}
|
||||
|
||||
Wt::WString updatePeriodDisplay(int row) {
|
||||
return boost::any_cast<Wt::WString>
|
||||
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::DisplayRole));
|
||||
}
|
||||
|
||||
|
||||
int getUpdateStartTimeModelRow(Wt::WString value)
|
||||
{
|
||||
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
|
||||
{
|
||||
if (updateStartTime(i) == value)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int getUpdateStartTimeModelRow(boost::posix_time::time_duration duration)
|
||||
{
|
||||
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
|
||||
{
|
||||
if (updateStartTimeDuration(i) == duration)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
boost::posix_time::time_duration updateStartTimeDuration(int row) {
|
||||
return boost::any_cast<boost::posix_time::time_duration>
|
||||
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::UserRole));
|
||||
}
|
||||
|
||||
Wt::WString updateStartTime(int row) {
|
||||
return boost::any_cast<Wt::WString>
|
||||
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::DisplayRole));
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
|
||||
void initializeModels() {
|
||||
|
||||
_updatePeriodModel = new Wt::WStringListModel(this);
|
||||
|
||||
_updatePeriodModel->addString("Never");
|
||||
_updatePeriodModel->setData(0, 0, std::string("never"), Wt::UserRole);
|
||||
|
||||
_updatePeriodModel->addString("Daily");
|
||||
_updatePeriodModel->setData(1, 0, std::string("daily"), Wt::UserRole);
|
||||
|
||||
_updatePeriodModel->addString("Weekly");
|
||||
_updatePeriodModel->setData(2, 0, std::string("weekly"), Wt::UserRole);
|
||||
|
||||
_updatePeriodModel->addString("Monthly");
|
||||
_updatePeriodModel->setData(3, 0, std::string("monthly"), Wt::UserRole);
|
||||
|
||||
_updateStartTimeModel = new Wt::WStringListModel(this);
|
||||
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
{
|
||||
boost::posix_time::time_duration dur = boost::posix_time::hours(i);
|
||||
|
||||
boost::posix_time::time_facet* facet = new boost::posix_time::time_facet("%H:%M");
|
||||
facet->time_duration_format("%H:%M");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss.imbue(std::locale(oss.getloc(), facet));
|
||||
|
||||
oss << dur;
|
||||
|
||||
_updateStartTimeModel->addString( oss.str() );
|
||||
_updateStartTimeModel->setData(i, 0, dur, Wt::UserRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Wt::WValidator *createUpdatePeriodValidator()
|
||||
{
|
||||
Wt::WValidator* v = new Wt::WValidator();
|
||||
v->setMandatory(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
Wt::WValidator *createStartTimeValidator()
|
||||
{
|
||||
Wt::WValidator* v = new Wt::WValidator();
|
||||
v->setMandatory(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
Wt::WValidator *createFileExtensionValidator()
|
||||
{
|
||||
Wt::WRegExpValidator *v = new Wt::WRegExpValidator("(?:\\.\\w+(?:\\s*))+");
|
||||
return v;
|
||||
}
|
||||
|
||||
Wt::WValidator *createMinProbabilityValidator()
|
||||
{
|
||||
Wt::WIntValidator *v = new Wt::WIntValidator(50, 100);
|
||||
v->setMandatory(true);
|
||||
return v;
|
||||
}
|
||||
|
||||
Wt::WStringListModel* _updatePeriodModel;
|
||||
Wt::WStringListModel* _updateStartTimeModel;
|
||||
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field DatabaseFormModel::UpdatePeriodField = "update-period";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::UpdateStartTimeField = "update-start-time";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::AudioFileExtensionsField = "audio-file-extensions";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::VideoFileExtensionsField = "video-file-extensions";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::TagsHighLevelAcousticBrainz = "tags-highlevel-acousticbrainz";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability = "tags-highlevel-acousticbrainz-min-probability";
|
||||
const Wt::WFormModel::Field DatabaseFormModel::TagsSimilarityAcousticBrainz = "tags-similarity-acousticbrainz";
|
||||
|
||||
|
||||
DatabaseFormView::DatabaseFormView(Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
_model = new DatabaseFormModel(this);
|
||||
|
||||
setTemplateText(tr("settings-database"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
_applyInfo = new Wt::WText();
|
||||
_applyInfo->setInline(false);
|
||||
_applyInfo->hide();
|
||||
bindWidget("apply-info", _applyInfo);
|
||||
|
||||
// Update Period
|
||||
Wt::WComboBox *updatePeriodCB = new Wt::WComboBox();
|
||||
setFormWidget(DatabaseFormModel::UpdatePeriodField, updatePeriodCB);
|
||||
updatePeriodCB->setModel(_model->updatePeriodModel());
|
||||
updatePeriodCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Update Start Time
|
||||
Wt::WComboBox *updateStartTimeCB = new Wt::WComboBox();
|
||||
setFormWidget(DatabaseFormModel::UpdateStartTimeField, updateStartTimeCB);
|
||||
updateStartTimeCB->setModel(_model->updateStartTimeModel());
|
||||
updateStartTimeCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Audio file extensions
|
||||
Wt::WLineEdit *audioFileExtensionsEdit = new Wt::WLineEdit();
|
||||
setFormWidget(DatabaseFormModel::AudioFileExtensionsField, audioFileExtensionsEdit);
|
||||
audioFileExtensionsEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Video file extensions
|
||||
Wt::WLineEdit *videoFileExtensionsEdit = new Wt::WLineEdit();
|
||||
setFormWidget(DatabaseFormModel::VideoFileExtensionsField, videoFileExtensionsEdit);
|
||||
videoFileExtensionsEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Tags from AB high level
|
||||
Wt::WCheckBox* highLevel = new Wt::WCheckBox();
|
||||
setFormWidget(DatabaseFormModel::TagsHighLevelAcousticBrainz, highLevel);
|
||||
|
||||
setFormWidget(DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability, new Wt::WSpinBox());
|
||||
|
||||
highLevel->changed().connect(std::bind([=] {
|
||||
_model->setReadOnly(DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability,
|
||||
!(highLevel->checkState() == Wt::Checked));
|
||||
updateModel(_model);
|
||||
updateViewField(_model, DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability);
|
||||
}));
|
||||
|
||||
// Tags from AB similarity
|
||||
setFormWidget(DatabaseFormModel::TagsSimilarityAcousticBrainz, new Wt::WCheckBox());
|
||||
|
||||
// Title & Buttons
|
||||
bindString("scan-title", "Scan settings");
|
||||
bindString("tags-title", "Tag settings");
|
||||
|
||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
||||
bindWidget("apply-button", saveButton);
|
||||
saveButton->setStyleClass("btn-primary");
|
||||
|
||||
Wt::WPushButton *discardButton = new Wt::WPushButton("Discard");
|
||||
bindWidget("discard-button", discardButton);
|
||||
|
||||
saveButton->clicked().connect(this, &DatabaseFormView::processSave);
|
||||
discardButton->clicked().connect(this, &DatabaseFormView::processDiscard);
|
||||
|
||||
Wt::WPushButton *immediateScanButton = new Wt::WPushButton("Immediate scan");
|
||||
immediateScanButton->setStyleClass("btn-warning");
|
||||
bindWidget("immediate-scan-button", immediateScanButton);
|
||||
immediateScanButton->clicked().connect(this, &DatabaseFormView::processImmediateScan);
|
||||
|
||||
updateView(_model);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
DatabaseFormView::processImmediateScan()
|
||||
{
|
||||
|
||||
_model->setImmediateScan();
|
||||
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Media folder scan has been started!" ) );
|
||||
_applyInfo->setStyleClass("alert alert-warning");
|
||||
_applyInfo->show();
|
||||
|
||||
_sigChanged.emit();
|
||||
}
|
||||
|
||||
void
|
||||
DatabaseFormView::processDiscard()
|
||||
{
|
||||
_applyInfo->show();
|
||||
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
||||
_applyInfo->setStyleClass("alert alert-info");
|
||||
|
||||
_model->loadData();
|
||||
|
||||
_model->validate();
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
void
|
||||
DatabaseFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
_applyInfo->show();
|
||||
|
||||
if (_model->validate()) {
|
||||
// Make the model to commit data into DB
|
||||
_model->saveData();
|
||||
|
||||
_sigChanged.emit();
|
||||
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
||||
_applyInfo->setStyleClass("alert alert-success");
|
||||
}
|
||||
else {
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
// Udate the view: Delete any validation message in the view, etc.
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -1,58 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_DB_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_DB_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class DatabaseFormModel;
|
||||
|
||||
class DatabaseFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
DatabaseFormView(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
Wt::Signal<void>& changed() { return _sigChanged; }
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigChanged;
|
||||
|
||||
void processSave();
|
||||
void processDiscard();
|
||||
void processImmediateScan();
|
||||
|
||||
Wt::WText *_applyInfo;
|
||||
DatabaseFormModel *_model;
|
||||
|
||||
};
|
||||
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,229 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/Auth/Identity>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsFirstConnectionFormView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class FirstConnectionFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field NameField;
|
||||
static const Field EmailField;
|
||||
static const Field PasswordField;
|
||||
static const Field PasswordConfirmField;
|
||||
|
||||
FirstConnectionFormModel(Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent)
|
||||
{
|
||||
addField(NameField);
|
||||
addField(EmailField);
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
|
||||
setValidator(NameField, createNameValidator());
|
||||
setValidator(EmailField, createEmailValidator());
|
||||
|
||||
}
|
||||
|
||||
bool saveData(Wt::WString& error)
|
||||
{
|
||||
// DBO transaction active here
|
||||
try {
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Check if a user already exist
|
||||
// If it's the case, just do nothing
|
||||
if (!Database::User::getAll(DboSession()).empty())
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Admin user already created";
|
||||
error = Wt::WString("Admin user already created!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create user
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().registerNew();
|
||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
||||
|
||||
// Account
|
||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
||||
|
||||
user.modify()->setAdmin( true );
|
||||
|
||||
}
|
||||
catch(Wt::Dbo::Exception& exception)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
||||
error = Wt::WString(exception.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
// DBO transaction active here
|
||||
|
||||
Wt::WString error;
|
||||
|
||||
if (field == PasswordField)
|
||||
{
|
||||
// Password is mandatory if we create the user
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
||||
valueText(NameField),
|
||||
valueText(EmailField).toUTF8());
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordConfirmField)
|
||||
{
|
||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
||||
{
|
||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply validators
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
|
||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
||||
|
||||
return validation(field).state() == Wt::WValidator::Valid;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field FirstConnectionFormModel::NameField = "name";
|
||||
const Wt::WFormModel::Field FirstConnectionFormModel::EmailField = "email";
|
||||
const Wt::WFormModel::Field FirstConnectionFormModel::PasswordField = "password";
|
||||
const Wt::WFormModel::Field FirstConnectionFormModel::PasswordConfirmField = "password-confirm";
|
||||
|
||||
FirstConnectionFormView::FirstConnectionFormView(Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
|
||||
_model = new FirstConnectionFormModel(this);
|
||||
|
||||
setTemplateText(tr("settings-first-connection"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
_applyInfo = new Wt::WText();
|
||||
_applyInfo->setInline(false);
|
||||
_applyInfo->hide();
|
||||
bindWidget("apply-info", _applyInfo);
|
||||
|
||||
// Name
|
||||
Wt::WLineEdit* accountEdit = new Wt::WLineEdit();
|
||||
setFormWidget(FirstConnectionFormModel::NameField, accountEdit);
|
||||
accountEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Email
|
||||
Wt::WLineEdit* emailEdit = new Wt::WLineEdit();
|
||||
setFormWidget(FirstConnectionFormModel::EmailField, emailEdit);
|
||||
emailEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Password
|
||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
||||
setFormWidget(FirstConnectionFormModel::PasswordField, passwordEdit );
|
||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
passwordEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Password confirmation
|
||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
||||
setFormWidget(FirstConnectionFormModel::PasswordConfirmField, passwordConfirmEdit);
|
||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
passwordConfirmEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Title & Buttons
|
||||
bindString("title", "Create Admin account");
|
||||
|
||||
_saveButton = new Wt::WPushButton("Create");
|
||||
_saveButton->setStyleClass("btn-primary");
|
||||
bindWidget("save-button", _saveButton);
|
||||
_saveButton->clicked().connect(this, &FirstConnectionFormView::processSave);
|
||||
|
||||
updateView(_model);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
FirstConnectionFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
_applyInfo->show();
|
||||
if (_model->validate())
|
||||
{
|
||||
Wt::WString error;
|
||||
// commit model into DB
|
||||
if (_model->saveData(error) )
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied! Please refresh this page in order to login"));
|
||||
_applyInfo->setStyleClass("alert alert-success");
|
||||
_saveButton->hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,52 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_FIRST_CONNECTION_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_FIRST_CONNECTION_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class FirstConnectionFormModel;
|
||||
|
||||
class FirstConnectionFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
FirstConnectionFormView(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
private:
|
||||
|
||||
void processSave();
|
||||
|
||||
Wt::WPushButton* _saveButton;
|
||||
FirstConnectionFormModel* _model;
|
||||
Wt::WText* _applyInfo;
|
||||
|
||||
};
|
||||
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,172 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WGroupBox>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WMessageBox>
|
||||
|
||||
#include "database/MediaDirectory.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsMediaDirectoryFormView.hpp"
|
||||
|
||||
#include "SettingsMediaDirectories.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
MediaDirectories::MediaDirectories(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
// Stack two widgets:
|
||||
_stack = new Wt::WStackedWidget(this);
|
||||
|
||||
// 1/ the media directory table view
|
||||
{
|
||||
Wt::WGroupBox *container = new Wt::WGroupBox("Media Folders", _stack);
|
||||
|
||||
_table = new Wt::WTable(container);
|
||||
|
||||
_table->addStyleClass("table form-inline");
|
||||
|
||||
_table->toggleStyleClass("table-hover", true);
|
||||
_table->toggleStyleClass("table-striped", true);
|
||||
|
||||
_table->setHeaderCount(1);
|
||||
|
||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("#"));
|
||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Path"));
|
||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("Type"));
|
||||
|
||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add Folder");
|
||||
addBtn->setStyleClass("btn-success");
|
||||
container->addWidget( addBtn );
|
||||
addBtn->clicked().connect(boost::bind(&MediaDirectories::handleCreateMediaDirectory, this));
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectories::refresh(void)
|
||||
{
|
||||
|
||||
assert(_table->rowCount() > 0);
|
||||
for (int i = _table->rowCount() - 1; i > 0; --i)
|
||||
_table->deleteRow(i);
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
std::vector<Database::MediaDirectory::pointer> mediaDirectories = Database::MediaDirectory::getAll(DboSession());
|
||||
|
||||
std::size_t id = 1;
|
||||
BOOST_FOREACH(Database::MediaDirectory::pointer mediaDirectory, mediaDirectories)
|
||||
{
|
||||
_table->elementAt(id, 0)->addWidget(new Wt::WText( Wt::WString::fromUTF8("{1}").arg(id)));
|
||||
_table->elementAt(id, 1)->addWidget(new Wt::WText( Wt::WString::fromUTF8(mediaDirectory->getPath().string()) ));
|
||||
|
||||
Wt::WString dirType;
|
||||
switch(mediaDirectory->getType())
|
||||
{
|
||||
case Database::MediaDirectory::Video: dirType = "Video"; break;
|
||||
case Database::MediaDirectory::Audio: dirType = "Audio"; break;
|
||||
}
|
||||
|
||||
_table->elementAt(id, 2)->addWidget( new Wt::WText( dirType ));
|
||||
|
||||
Wt::WPushButton* delBtn = new Wt::WPushButton("Delete");
|
||||
delBtn->setStyleClass("btn-danger");
|
||||
_table->elementAt(id, 3)->addWidget(delBtn);
|
||||
delBtn->clicked().connect(boost::bind( &MediaDirectories::handleDelMediaDirectory, this, mediaDirectory->getPath(), mediaDirectory->getType() ) );
|
||||
|
||||
++id;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectories::handleDelMediaDirectory(boost::filesystem::path p, Database::MediaDirectory::Type type)
|
||||
{
|
||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
||||
("Delete Folder",
|
||||
Wt::WString( "Deleting folder '{1}'?").arg(p.string()),
|
||||
Wt::Question, Wt::Yes | Wt::No);
|
||||
|
||||
messageBox->setModal(true);
|
||||
|
||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
||||
if (messageBox->buttonResult() == Wt::Yes)
|
||||
{
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Delete the media diretory
|
||||
Database::MediaDirectory::pointer mediaDirectory = Database::MediaDirectory::get(DboSession(), p, type);
|
||||
if (mediaDirectory)
|
||||
mediaDirectory.remove();
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
// Emit something changed in the settings
|
||||
_sigChanged.emit();
|
||||
}
|
||||
|
||||
delete messageBox;
|
||||
}));
|
||||
|
||||
messageBox->show();
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectories::handleCreateMediaDirectory(void)
|
||||
{
|
||||
assert(_stack->count() == 1);
|
||||
|
||||
MediaDirectoryFormView* formView = new MediaDirectoryFormView(_stack);
|
||||
formView->completed().connect(this, &MediaDirectories::handleMediaDirectoryFormCompleted);
|
||||
|
||||
_stack->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectories::handleMediaDirectoryFormCompleted(bool changed)
|
||||
{
|
||||
_stack->setCurrentIndex(0);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
// Refresh the user table if a change has been made
|
||||
refresh();
|
||||
|
||||
// Emit something changed in the settings
|
||||
_sigChanged.emit();
|
||||
}
|
||||
|
||||
// Delete the form view
|
||||
delete _stack->widget(1);
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
} // namespace Settings
|
||||
|
||||
|
||||
@@ -1,59 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_MEDIA_DIRECTORIES_HPP
|
||||
#define UI_SETTINGS_MEDIA_DIRECTORIES_HPP
|
||||
|
||||
#include <Wt/WStackedWidget>
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTable>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "database/MediaDirectory.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class MediaDirectories : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
MediaDirectories(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void refresh();
|
||||
|
||||
Wt::Signal<void>& changed() { return _sigChanged; }
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<void> _sigChanged;
|
||||
|
||||
void handleMediaDirectoryFormCompleted(bool changed);
|
||||
|
||||
void handleDelMediaDirectory(boost::filesystem::path p, Database::MediaDirectory::Type type);
|
||||
void handleCreateMediaDirectory(void);
|
||||
|
||||
Wt::WStackedWidget* _stack;
|
||||
Wt::WTable* _table;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
} // namespace Settings
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,185 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WStringListModel>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/MediaDirectory.hpp"
|
||||
|
||||
#include "common/DirectoryValidator.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsMediaDirectoryFormView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
// TODO validate if directory already exists
|
||||
class MediaDirectoryFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field PathField;
|
||||
static const Field TypeField;
|
||||
|
||||
MediaDirectoryFormModel(Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent)
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
addField(PathField);
|
||||
addField(TypeField);
|
||||
|
||||
DirectoryValidator* dirValidator = new DirectoryValidator();
|
||||
dirValidator->setMandatory(true);
|
||||
setValidator(PathField, dirValidator);
|
||||
setValidator(TypeField, new Wt::WValidator(true)); // mandatory
|
||||
|
||||
}
|
||||
|
||||
Wt::WAbstractItemModel *typeModel() { return _typeModel; }
|
||||
|
||||
bool saveData(Wt::WString& error)
|
||||
{
|
||||
try {
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Database::MediaDirectory::Type type
|
||||
= (valueText(TypeField) == "Audio") ? Database::MediaDirectory::Audio : Database::MediaDirectory::Video;
|
||||
|
||||
if (Database::MediaDirectory::get(DboSession(), valueText(PathField).toUTF8(), type))
|
||||
{
|
||||
error = "This Path/Type already exists!";
|
||||
return false;
|
||||
}
|
||||
|
||||
Database::MediaDirectory::create(DboSession(), valueText(PathField).toUTF8(), type);
|
||||
|
||||
}
|
||||
catch(Wt::Dbo::Exception& exception)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void initializeModels()
|
||||
{
|
||||
_typeModel = new Wt::WStringListModel(this);
|
||||
_typeModel->addString("Audio");
|
||||
_typeModel->addString("Video");
|
||||
}
|
||||
|
||||
std::string _userId;
|
||||
Wt::WStringListModel* _typeModel;
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field MediaDirectoryFormModel::PathField = "path";
|
||||
const Wt::WFormModel::Field MediaDirectoryFormModel::TypeField = "type";
|
||||
|
||||
MediaDirectoryFormView::MediaDirectoryFormView(Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
|
||||
_model = new MediaDirectoryFormModel(this);
|
||||
|
||||
setTemplateText(tr("settings-new-media-directory"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
_applyInfo = new Wt::WText();
|
||||
_applyInfo->setInline(false);
|
||||
_applyInfo->hide();
|
||||
bindWidget("apply-info", _applyInfo);
|
||||
|
||||
// Path
|
||||
Wt::WLineEdit* pathEdit = new Wt::WLineEdit();
|
||||
setFormWidget(MediaDirectoryFormModel::PathField, pathEdit);
|
||||
pathEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Type
|
||||
Wt::WComboBox *typeCB = new Wt::WComboBox();
|
||||
setFormWidget(MediaDirectoryFormModel::TypeField, typeCB);
|
||||
typeCB->setModel(_model->typeModel());
|
||||
typeCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
||||
|
||||
// Title & Buttons
|
||||
bindString("title", "Add media folder");
|
||||
|
||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Add");
|
||||
saveButton->setStyleClass("btn-primary");
|
||||
bindWidget("save-button", saveButton);
|
||||
saveButton->clicked().connect(this, &MediaDirectoryFormView::processSave);
|
||||
|
||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Cancel");
|
||||
bindWidget("cancel-button", cancelButton);
|
||||
cancelButton->clicked().connect(this, &MediaDirectoryFormView::processCancel);
|
||||
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectoryFormView::processCancel()
|
||||
{
|
||||
// parent widget will delete this widget
|
||||
completed().emit(false);
|
||||
}
|
||||
|
||||
void
|
||||
MediaDirectoryFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
_applyInfo->show();
|
||||
if (_model->validate())
|
||||
{
|
||||
Wt::WString error;
|
||||
// commit model into DB
|
||||
if (_model->saveData(error) ) {
|
||||
completed().emit(true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
||||
_applyInfo->setStyleClass("alert alert-danger");
|
||||
}
|
||||
|
||||
updateView(_model);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,54 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_MEDIA_DIR_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_MEDIA_DIR_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class MediaDirectoryFormModel;
|
||||
|
||||
class MediaDirectoryFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
MediaDirectoryFormView(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
// Signal emitted once the form is completed
|
||||
Wt::Signal<bool>& completed() { return _sigCompleted; }
|
||||
|
||||
private:
|
||||
void processCancel(); // reload from DB
|
||||
void processSave(); // commit into DB
|
||||
|
||||
Wt::Signal<bool> _sigCompleted;
|
||||
MediaDirectoryFormModel* _model;
|
||||
Wt::WText* _applyInfo;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,411 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WStringListModel>
|
||||
#include <Wt/WFormModel>
|
||||
#include <Wt/WLineEdit>
|
||||
#include <Wt/WCheckBox>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/Auth/Identity>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsUserFormView.hpp"
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class UserFormModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field NameField;
|
||||
static const Field EmailField;
|
||||
static const Field PasswordField;
|
||||
static const Field PasswordConfirmField;
|
||||
static const Field AdminField;
|
||||
static const Field AudioBitrateLimitField;
|
||||
static const Field VideoBitrateLimitField;
|
||||
|
||||
UserFormModel(std::string userId, Wt::WObject *parent = 0)
|
||||
: Wt::WFormModel(parent),
|
||||
_userId(userId)
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
addField(NameField);
|
||||
addField(EmailField);
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
addField(AdminField);
|
||||
addField(AudioBitrateLimitField);
|
||||
addField(VideoBitrateLimitField);
|
||||
|
||||
setValidator(NameField, createNameValidator());
|
||||
setValidator(EmailField, createEmailValidator());
|
||||
// If creating a user, passwords are mandatory
|
||||
if (_userId.empty())
|
||||
{
|
||||
setValidator(PasswordField, new Wt::WValidator(true)); // mandatory
|
||||
setValidator(PasswordConfirmField, new Wt::WValidator(true)); // mandatory
|
||||
}
|
||||
setValidator(AudioBitrateLimitField, new Wt::WValidator(true)); // mandatory
|
||||
setValidator(VideoBitrateLimitField, new Wt::WValidator(true)); // mandatory
|
||||
|
||||
// populate the model with initial data
|
||||
loadData(userId);
|
||||
}
|
||||
|
||||
Wt::WAbstractItemModel *audioBitrateModel() { return _audioBitrateModel; }
|
||||
Wt::WAbstractItemModel *videoBitrateModel() { return _videoBitrateModel; }
|
||||
|
||||
void loadData(std::string userId)
|
||||
{
|
||||
|
||||
if (!userId.empty())
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( userId );
|
||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
||||
|
||||
Wt::Auth::User currentUser = CurrentAuthUser();
|
||||
|
||||
if (user && authUser.isValid())
|
||||
{
|
||||
if (user->isAdmin())
|
||||
{
|
||||
setValue(AdminField, true);
|
||||
|
||||
// We can cannot remove admin rights to ourselves
|
||||
if (currentUser == authUser)
|
||||
setReadOnly(AdminField, true);
|
||||
|
||||
// if the user is admin, no need to limit it
|
||||
setReadOnly(AudioBitrateLimitField, true);
|
||||
setValidator(AudioBitrateLimitField, nullptr);
|
||||
|
||||
setReadOnly(VideoBitrateLimitField, true);
|
||||
setValidator(VideoBitrateLimitField, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
setValue(AudioBitrateLimitField, user->getMaxAudioBitrate() / 1000); // in kbps
|
||||
setValue(VideoBitrateLimitField, user->getMaxVideoBitrate() / 1000); // in kbps
|
||||
}
|
||||
|
||||
setValue(NameField, authUser.identity(Wt::Auth::Identity::LoginName));
|
||||
if (!authUser.email().empty())
|
||||
setValue(EmailField, authUser.email());
|
||||
else
|
||||
setValue(EmailField, authUser.unverifiedEmail());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool saveData()
|
||||
{
|
||||
try {
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
if (_userId.empty())
|
||||
{
|
||||
// Create user
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().registerNew();
|
||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
||||
|
||||
// Account
|
||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
||||
|
||||
// Access
|
||||
{
|
||||
boost::any v = value(AdminField);
|
||||
if (!v.empty() && boost::any_cast<bool>(v) == true)
|
||||
user.modify()->setAdmin( true );
|
||||
else
|
||||
user.modify()->setAdmin( false );
|
||||
}
|
||||
|
||||
if (!isReadOnly(AudioBitrateLimitField))
|
||||
user.modify()->setMaxAudioBitrate( Wt::asNumber(value(AudioBitrateLimitField)) * 1000); // in bps
|
||||
|
||||
if (!isReadOnly(VideoBitrateLimitField))
|
||||
user.modify()->setMaxVideoBitrate( Wt::asNumber(value(VideoBitrateLimitField)) * 1000); // in bps
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update user
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId(_userId);
|
||||
Database::User::pointer user = DbHandler().getUser( authUser );
|
||||
|
||||
// user may have been deleted by someone else
|
||||
if (!authUser.isValid()) {
|
||||
LMS_LOG(UI, ERROR) << "user identity does not exist!";
|
||||
return false;
|
||||
}
|
||||
else if(!user)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "User not found!";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Account
|
||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
||||
|
||||
// Password
|
||||
if (!valueText(PasswordField).empty())
|
||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
||||
|
||||
// Access
|
||||
if (!isReadOnly(AdminField))
|
||||
{
|
||||
boost::any v = value(AdminField);
|
||||
if (!v.empty() && boost::any_cast<bool>(v) == true)
|
||||
user.modify()->setAdmin( true );
|
||||
else
|
||||
user.modify()->setAdmin( false );
|
||||
}
|
||||
|
||||
if (!isReadOnly(AudioBitrateLimitField))
|
||||
user.modify()->setMaxAudioBitrate( Wt::asNumber(value(AudioBitrateLimitField)) * 1000); // in bps
|
||||
|
||||
if (!isReadOnly(VideoBitrateLimitField))
|
||||
user.modify()->setMaxVideoBitrate( Wt::asNumber(value(VideoBitrateLimitField)) * 1000); // in bps
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
}
|
||||
catch(Wt::Dbo::Exception& exception)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
Wt::WString error;
|
||||
|
||||
if (field == NameField)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
// Must be unique since used as LoginIdentity
|
||||
Wt::Auth::User user = DbHandler().getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, valueText(field));
|
||||
if (user.isValid() && user.id() != _userId)
|
||||
error = "Already exists";
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordField)
|
||||
{
|
||||
// Password is mandatory if we create the user
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
||||
valueText(NameField),
|
||||
valueText(EmailField).toUTF8());
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordConfirmField)
|
||||
{
|
||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
||||
{
|
||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply validators
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
|
||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
||||
|
||||
return validation(field).state() == Wt::WValidator::Valid;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
|
||||
void initializeModels()
|
||||
{
|
||||
// AUDIO
|
||||
_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(this);
|
||||
BOOST_FOREACH(std::size_t bitrate, Database::User::videoBitrates)
|
||||
_videoBitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps
|
||||
|
||||
}
|
||||
|
||||
std::string _userId;
|
||||
Wt::WStringListModel* _audioBitrateModel;
|
||||
Wt::WStringListModel* _videoBitrateModel;
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field UserFormModel::NameField = "name";
|
||||
const Wt::WFormModel::Field UserFormModel::EmailField = "email";
|
||||
const Wt::WFormModel::Field UserFormModel::PasswordField = "password";
|
||||
const Wt::WFormModel::Field UserFormModel::PasswordConfirmField = "password-confirm";
|
||||
const Wt::WFormModel::Field UserFormModel::AdminField = "admin";
|
||||
const Wt::WFormModel::Field UserFormModel::AudioBitrateLimitField = "audio-bitrate-limit";
|
||||
const Wt::WFormModel::Field UserFormModel::VideoBitrateLimitField = "video-bitrate-limit";
|
||||
|
||||
|
||||
UserFormView::UserFormView(std::string userId, Wt::WContainerWidget *parent)
|
||||
: Wt::WTemplateFormView(parent)
|
||||
{
|
||||
|
||||
_model = new UserFormModel(userId, this);
|
||||
|
||||
setTemplateText(tr("settings-new-user"));
|
||||
addFunction("id", &WTemplate::Functions::id);
|
||||
addFunction("block", &WTemplate::Functions::id);
|
||||
|
||||
// Name
|
||||
setFormWidget(UserFormModel::NameField, new Wt::WLineEdit());
|
||||
|
||||
// Email
|
||||
setFormWidget(UserFormModel::EmailField, new Wt::WLineEdit());
|
||||
|
||||
// Password
|
||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
||||
setFormWidget(UserFormModel::PasswordField, passwordEdit );
|
||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
|
||||
// Password confirmation
|
||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
||||
setFormWidget(UserFormModel::PasswordConfirmField, passwordConfirmEdit);
|
||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
||||
|
||||
bindString("access", "Access");
|
||||
|
||||
// Admin Field
|
||||
setFormWidget(UserFormModel::AdminField, new Wt::WCheckBox());
|
||||
|
||||
// AudioBitrate
|
||||
Wt::WComboBox *audioBitrateCB = new Wt::WComboBox();
|
||||
setFormWidget(UserFormModel::AudioBitrateLimitField, audioBitrateCB);
|
||||
audioBitrateCB->setStyleClass("span2");
|
||||
audioBitrateCB->setModel(_model->audioBitrateModel());
|
||||
|
||||
// VideoBitrate
|
||||
Wt::WComboBox *videoBitrateCB = new Wt::WComboBox();
|
||||
setFormWidget(UserFormModel::VideoBitrateLimitField, videoBitrateCB);
|
||||
videoBitrateCB->setStyleClass("span2");
|
||||
videoBitrateCB->setModel(_model->videoBitrateModel());
|
||||
|
||||
// Title & Buttons
|
||||
Wt::WString title;
|
||||
if (userId.empty()) {
|
||||
title = Wt::WString("Create user");
|
||||
}
|
||||
else {
|
||||
Wt::Dbo::Transaction transaction (DboSession());
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( userId );
|
||||
|
||||
Wt::WString userName;
|
||||
if (authUser.isValid())
|
||||
userName = authUser.identity(Wt::Auth::Identity::LoginName);
|
||||
else
|
||||
; // TODO display user deleted and close the widget
|
||||
|
||||
title = Wt::WString("Edit user {1}").arg(userName);
|
||||
}
|
||||
|
||||
bindString("title", title);
|
||||
|
||||
Wt::WPushButton *saveButton = new Wt::WPushButton();
|
||||
if (userId.empty()) {
|
||||
saveButton->setText("Create user");
|
||||
saveButton->setStyleClass("btn-success");
|
||||
}
|
||||
else
|
||||
{
|
||||
saveButton->setText("Save");
|
||||
saveButton->setStyleClass("btn-primary");
|
||||
}
|
||||
bindWidget("save-button", saveButton);
|
||||
saveButton->clicked().connect(this, &UserFormView::processSave);
|
||||
|
||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Cancel");
|
||||
bindWidget("cancel-button", cancelButton);
|
||||
cancelButton->clicked().connect(this, &UserFormView::processCancel);
|
||||
|
||||
updateView(_model);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
UserFormView::processCancel()
|
||||
{
|
||||
// parent widget will delete this widget
|
||||
completed().emit(false);
|
||||
}
|
||||
|
||||
void
|
||||
UserFormView::processSave()
|
||||
{
|
||||
updateModel(_model);
|
||||
|
||||
|
||||
if (_model->validate())
|
||||
{
|
||||
// commit model into DB
|
||||
if (_model->saveData() )
|
||||
{
|
||||
// parent widget will delete this widget
|
||||
completed().emit(true);
|
||||
}
|
||||
// else TODO display a nice error message
|
||||
}
|
||||
else
|
||||
{
|
||||
updateView(_model);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
@@ -1,55 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_USER_FORM_VIEW_HPP
|
||||
#define UI_SETTINGS_USER_FORM_VIEW_HPP
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTemplateFormView>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class UserFormModel;
|
||||
|
||||
class UserFormView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
|
||||
UserFormView(std::string userId, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
// Signal emitted once the form is completed
|
||||
Wt::Signal<bool>& completed() { return _sigCompleted; }
|
||||
|
||||
private:
|
||||
|
||||
Wt::Signal<bool> _sigCompleted;
|
||||
|
||||
void processSave();
|
||||
void processCancel();
|
||||
|
||||
UserFormModel* _model;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
@@ -1,202 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#include <Wt/WGroupBox>
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WMessageBox>
|
||||
|
||||
#include <Wt/Auth/Identity>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "SettingsUserFormView.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "SettingsUsers.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
Users::Users(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
// Stack two widgets:
|
||||
_stack = new Wt::WStackedWidget(this);
|
||||
|
||||
// 1/ the user table view
|
||||
{
|
||||
Wt::WGroupBox *container = new Wt::WGroupBox("Users", _stack);
|
||||
|
||||
_table = new Wt::WTable(container);
|
||||
|
||||
_table->addStyleClass("table form-inline");
|
||||
|
||||
_table->toggleStyleClass("table-hover", true);
|
||||
_table->toggleStyleClass("table-striped", true);
|
||||
|
||||
_table->setHeaderCount(1);
|
||||
|
||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("#"));
|
||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Name"));
|
||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("e-Mail"));
|
||||
_table->elementAt(0, 3)->addWidget(new Wt::WText("Admin"));
|
||||
|
||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add User");
|
||||
addBtn->setStyleClass("btn-success");
|
||||
container->addWidget( addBtn );
|
||||
addBtn->clicked().connect(boost::bind(&Users::handleCreateUser, this, ""));
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
Users::refresh(void)
|
||||
{
|
||||
|
||||
assert(_table->rowCount() > 0);
|
||||
for (int i = _table->rowCount() - 1; i > 0; --i)
|
||||
_table->deleteRow(i);
|
||||
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
const Wt::Auth::User& currentUser = CurrentAuthUser();
|
||||
|
||||
std::vector<Database::User::pointer> users = Database::User::getAll(DboSession());
|
||||
|
||||
std::size_t userIndex = 1;
|
||||
for (std::size_t i = 0; i < users.size(); ++i)
|
||||
{
|
||||
|
||||
std::string userId = Database::User::getId(users[i]);
|
||||
|
||||
Wt::Auth::User authUser;
|
||||
|
||||
// Hack try/catch here since it may fail!
|
||||
try {
|
||||
authUser = DbHandler().getUserDatabase().findWithId( userId );
|
||||
}
|
||||
catch(Wt::Dbo::Exception& e)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Caught exception when getting userId=" << userId << ": " << e.code();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!authUser.isValid()) {
|
||||
LMS_LOG(UI, ERROR) << "Users::refresh: skipping invalid userId = " << userId;
|
||||
continue;
|
||||
}
|
||||
|
||||
_table->elementAt(userIndex, 0)->addWidget(new Wt::WText( Wt::WString::fromUTF8("{1}").arg(userIndex)));
|
||||
_table->elementAt(userIndex, 1)->addWidget(new Wt::WText( authUser.identity(Wt::Auth::Identity::LoginName)) );
|
||||
|
||||
Wt::WText* email = new Wt::WText();
|
||||
if (!authUser.email().empty()) {
|
||||
email->setText(authUser.email());
|
||||
}
|
||||
else {
|
||||
email->setStyleClass("alert-danger");
|
||||
email->setText(authUser.unverifiedEmail());
|
||||
}
|
||||
_table->elementAt(userIndex, 2)->addWidget( email ) ;
|
||||
_table->elementAt(userIndex, 3)->addWidget(new Wt::WText( users[i]->isAdmin() ? "Yes" : "No" ));
|
||||
|
||||
Wt::WPushButton* editBtn = new Wt::WPushButton("Edit");
|
||||
_table->elementAt(userIndex, 4)->addWidget(editBtn);
|
||||
editBtn->clicked().connect(boost::bind( &Users::handleCreateUser, this, userId));
|
||||
|
||||
if (currentUser != authUser)
|
||||
{
|
||||
Wt::WPushButton* delBtn = new Wt::WPushButton("Delete");
|
||||
delBtn->setStyleClass("btn-danger");
|
||||
delBtn->setMargin(5, Wt::Left);
|
||||
_table->elementAt(userIndex, 4)->addWidget(delBtn);
|
||||
delBtn->clicked().connect(boost::bind( &Users::handleDelUser, this, authUser.identity(Wt::Auth::Identity::LoginName), userId));
|
||||
}
|
||||
|
||||
++userIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
Users::handleDelUser(Wt::WString loginNameIdentity, std::string id)
|
||||
{
|
||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
||||
("Delete User",
|
||||
Wt::WString( "Deleting user '{1}'?").arg(loginNameIdentity),
|
||||
Wt::Question, Wt::Yes | Wt::No);
|
||||
|
||||
messageBox->setModal(true);
|
||||
|
||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
||||
if (messageBox->buttonResult() == Wt::Yes)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
// Delete the user
|
||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( id );
|
||||
|
||||
DbHandler().getUserDatabase().deleteUser( authUser );
|
||||
|
||||
Database::User::pointer user = Database::User::getById(DboSession(), id);
|
||||
if (user)
|
||||
user.remove();
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
delete messageBox;
|
||||
}));
|
||||
|
||||
messageBox->show();
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
Users::handleCreateUser(std::string id)
|
||||
{
|
||||
assert(_stack->count() == 1);
|
||||
|
||||
UserFormView* userFormView = new UserFormView(id, _stack);
|
||||
userFormView->completed().connect(this, &Users::handleUserFormCompleted);
|
||||
|
||||
_stack->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
void
|
||||
Users::handleUserFormCompleted(bool changed)
|
||||
{
|
||||
_stack->setCurrentIndex(0);
|
||||
|
||||
// Refresh the user table if a change has been made
|
||||
if (changed)
|
||||
refresh();
|
||||
|
||||
// Delete the form view
|
||||
delete _stack->widget(1);
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
} // namespace Settings
|
||||
|
||||
|
||||
@@ -1,52 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef UI_SETTINGS_USERS_HPP
|
||||
#define UI_SETTINGS_USERS_HPP
|
||||
|
||||
#include <Wt/WStackedWidget>
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WTable>
|
||||
|
||||
namespace UserInterface {
|
||||
namespace Settings {
|
||||
|
||||
class Users : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
Users(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
|
||||
void handleUserFormCompleted(bool changed);
|
||||
|
||||
void handleDelUser(Wt::WString loginNameIdentity, std::string id);
|
||||
void handleCreateUser(std::string id); // set the id in order to edit the user
|
||||
|
||||
Wt::WStackedWidget* _stack;
|
||||
Wt::WTable* _table;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
} // namespace Settings
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,143 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
#include <Wt/WText>
|
||||
#include <Wt/WPushButton>
|
||||
|
||||
#include <Wt/WMediaPlayer>
|
||||
#include <Wt/WFileResource>
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "VideoDatabaseWidget.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
VideoDatabaseWidget::VideoDatabaseWidget(Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
_table = new Wt::WTable( this );
|
||||
_table->setHeaderCount(1);
|
||||
|
||||
_table->addStyleClass("table form-inline");
|
||||
|
||||
_table->toggleStyleClass("table-hover", true);
|
||||
_table->toggleStyleClass("table-striped", true);
|
||||
|
||||
updateView( boost::filesystem::path(), 0);
|
||||
}
|
||||
|
||||
void
|
||||
VideoDatabaseWidget::addHeader(void)
|
||||
{
|
||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("Name"));
|
||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Duration"));
|
||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("Action"));
|
||||
}
|
||||
|
||||
void
|
||||
VideoDatabaseWidget::addDirectory(const std::string& name, boost::filesystem::path path, size_t depth)
|
||||
{
|
||||
|
||||
int row = _table->rowCount();
|
||||
|
||||
new Wt::WText(Wt::WString::fromUTF8( name ), _table->elementAt(row, 0));
|
||||
new Wt::WText(Wt::WString::fromUTF8( " " ), _table->elementAt(row, 1));
|
||||
|
||||
Wt::WPushButton* btn = new Wt::WPushButton(Wt::WString::fromUTF8( "Open"), _table->elementAt(row, 2));
|
||||
|
||||
btn->clicked().connect(std::bind([=] () {
|
||||
updateView(path, depth);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
VideoDatabaseWidget::addVideo(const std::string& name, const boost::posix_time::time_duration& duration, const boost::filesystem::path& path)
|
||||
{
|
||||
|
||||
int row = _table->rowCount();
|
||||
|
||||
new Wt::WText(Wt::WString::fromUTF8( name ), _table->elementAt(row, 0));
|
||||
new Wt::WText(Wt::WString::fromUTF8( boost::posix_time::to_simple_string( duration )), _table->elementAt(row, 1));
|
||||
|
||||
Wt::WPushButton* btn = new Wt::WPushButton(Wt::WString::fromUTF8( "Play"), _table->elementAt(row, 2));
|
||||
|
||||
btn->clicked().connect(std::bind([=] () {
|
||||
playVideo().emit(path);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
VideoDatabaseWidget::updateView(boost::filesystem::path directory, size_t depth)
|
||||
{
|
||||
_table->clear();
|
||||
addHeader();
|
||||
|
||||
// If directory is not valid, add the root Media Directories
|
||||
if (depth == 0)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction ( DboSession() );
|
||||
|
||||
std::vector<Database::MediaDirectory::pointer> dirs
|
||||
= Database::MediaDirectory::getByType(DboSession(), Database::MediaDirectory::Video);
|
||||
|
||||
BOOST_FOREACH(Database::MediaDirectory::pointer dir, dirs)
|
||||
{
|
||||
addDirectory( dir->getPath().filename().string(),
|
||||
dir->getPath(),
|
||||
depth + 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
addDirectory( "..", directory.parent_path(), depth - 1);
|
||||
|
||||
// Iterators over files in the directory
|
||||
// If directory, add a directory entry
|
||||
// If video, add a video entry
|
||||
std::vector<boost::filesystem::path> paths;
|
||||
std::copy(boost::filesystem::directory_iterator(directory), boost::filesystem::directory_iterator(), std::back_inserter(paths));
|
||||
|
||||
std::sort(paths.begin(), paths.end());
|
||||
|
||||
BOOST_FOREACH(const boost::filesystem::path& path, paths)
|
||||
{
|
||||
if (boost::filesystem::is_directory(path))
|
||||
addDirectory( path.filename().string(), path, depth + 1);
|
||||
else if (boost::filesystem::is_regular(path) )
|
||||
{
|
||||
Wt::Dbo::Transaction transaction ( DboSession() );
|
||||
|
||||
Database::Video::pointer video = Database::Video::getByPath( DboSession(), path);
|
||||
if (video)
|
||||
addVideo( video->getName(), video->getDuration(), path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,52 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef VIDEO_DB_WIDGET_HPP
|
||||
#define VIDEO_DB_WIDGET_HPP
|
||||
|
||||
#include <Wt/WTable>
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class VideoDatabaseWidget : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
VideoDatabaseWidget(Wt::WContainerWidget *parent = 0);
|
||||
|
||||
// Signals
|
||||
Wt::Signal< boost::filesystem::path >& playVideo() { return _playVideo; }
|
||||
|
||||
private:
|
||||
|
||||
void addHeader(void);
|
||||
void addDirectory(const std::string& name, boost::filesystem::path path, size_t depth);
|
||||
void addVideo(const std::string& name, const boost::posix_time::time_duration& duration, const boost::filesystem::path& path);
|
||||
|
||||
void updateView(boost::filesystem::path directory, size_t depth);
|
||||
|
||||
Wt::Signal< boost::filesystem::path > _playVideo;
|
||||
|
||||
Wt::WTable* _table;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,200 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp> //include all types plus i/o
|
||||
|
||||
#include <Wt/WMediaPlayer>
|
||||
|
||||
#include "VideoParametersDialog.hpp"
|
||||
|
||||
#include "VideoMediaPlayerWidget.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Wt::WMediaPlayer::Encoding
|
||||
AvEncoding_to_WtEncoding(Av::Encoding encoding)
|
||||
{
|
||||
switch( encoding )
|
||||
{
|
||||
case Av::Encoding::OGA: return Wt::WMediaPlayer::OGA;
|
||||
case Av::Encoding::OGV: return Wt::WMediaPlayer::OGV;
|
||||
case Av::Encoding::MP3: return Wt::WMediaPlayer::MP3;
|
||||
case Av::Encoding::WEBMA: return Wt::WMediaPlayer::WEBMA;
|
||||
case Av::Encoding::WEBMV: return Wt::WMediaPlayer::WEBMV;
|
||||
case Av::Encoding::FLA: return Wt::WMediaPlayer::FLA;
|
||||
case Av::Encoding::FLV: return Wt::WMediaPlayer::FLV;
|
||||
case Av::Encoding::M4A: return Wt::WMediaPlayer::M4A;
|
||||
case Av::Encoding::M4V: return Wt::WMediaPlayer::M4V;
|
||||
}
|
||||
assert(0);
|
||||
}
|
||||
|
||||
|
||||
VideoMediaPlayerWidget::VideoMediaPlayerWidget( const Av::MediaFile& mediaFile, Av::TranscodeParameters parameters, Wt::WContainerWidget *parent)
|
||||
: Wt::WContainerWidget(parent),
|
||||
_mediaResource(nullptr),
|
||||
_currentParameters(parameters),
|
||||
_dialog(nullptr)
|
||||
{
|
||||
_mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Video, this );
|
||||
// _mediaPlayer->setAlternativeContent (new Wt::WText("You don't have HTML5 audio support!"));
|
||||
// _mediaPlayer->setOptions( Wt::WMediaPlayer::Autoplay );
|
||||
// _mediaPlayer->addSource( Wt::WMediaPlayer::WEBMV, "" );
|
||||
|
||||
{
|
||||
Wt::WContainerWidget *container = new Wt::WContainerWidget(this);
|
||||
|
||||
_playBtn = new Wt::WPushButton("Play", container );
|
||||
_pauseBtn = new Wt::WPushButton("Pause", 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);
|
||||
_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, &VideoMediaPlayerWidget::handleTimeUpdated);
|
||||
|
||||
Wt::WPushButton* fullScreenButton = new Wt::WPushButton("Fullscreen", container);
|
||||
_mediaPlayer->setButton(Wt::WMediaPlayer::FullScreen, fullScreenButton);
|
||||
|
||||
Wt::WPushButton* restoreScreenButton = new Wt::WPushButton("Restore screen", container);
|
||||
_mediaPlayer->setButton(Wt::WMediaPlayer::RestoreScreen, restoreScreenButton);
|
||||
|
||||
_mediaPlayer->timeUpdated().connect(this, &VideoMediaPlayerWidget::handleTimeUpdated);
|
||||
|
||||
_timeSlider->valueChanged().connect(this, &VideoMediaPlayerWidget::handlePlayOffset);
|
||||
_timeSlider->sliderMoved().connect(this, &VideoMediaPlayerWidget::handleSliderMoved);
|
||||
|
||||
_volumeSlider->sliderMoved().connect(this, &VideoMediaPlayerWidget::handleVolumeSliderMoved);
|
||||
}
|
||||
|
||||
|
||||
Wt::WPushButton* closeButton = new Wt::WPushButton("Close", this);
|
||||
closeButton->clicked().connect( this, &VideoMediaPlayerWidget::handleClose );
|
||||
|
||||
Wt::WPushButton* parametersButton = new Wt::WPushButton("Parameters", this);
|
||||
parametersButton->clicked().connect( this, &VideoMediaPlayerWidget::handleParametersEdit );
|
||||
|
||||
_currentFile = mediaFile.getPath();
|
||||
|
||||
load(parameters);
|
||||
|
||||
_timeSlider->setRange(0, mediaFile.getDuration().total_seconds() );
|
||||
_duration->setText( boost::posix_time::to_simple_string( mediaFile.getDuration() ));
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::load(Av::TranscodeParameters parameters)
|
||||
{
|
||||
_mediaPlayer->clearSources();
|
||||
|
||||
_currentParameters = parameters;
|
||||
|
||||
_mediaInternalLink.setResource( nullptr );
|
||||
if (_mediaResource)
|
||||
delete _mediaResource;
|
||||
|
||||
_mediaResource = new AvConvTranscodeStreamResource( _currentFile, parameters, this );
|
||||
_mediaInternalLink.setResource( _mediaResource );
|
||||
|
||||
_mediaPlayer->addSource( AvEncoding_to_WtEncoding(parameters.getEncoding()), _mediaInternalLink );
|
||||
|
||||
_timeSlider->setValue( 0 );
|
||||
|
||||
|
||||
_mediaPlayer->play();
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handlePlayOffset(int offsetSecs)
|
||||
{
|
||||
_currentParameters.setOffset( boost::posix_time::seconds(offsetSecs) );
|
||||
load( _currentParameters );
|
||||
|
||||
_timeSlider->setValue( offsetSecs );
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleSliderMoved(int value)
|
||||
{
|
||||
_curTime->setText( boost::posix_time::to_simple_string( boost::posix_time::seconds( value ) ) );
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleTimeUpdated(void)
|
||||
{
|
||||
|
||||
boost::posix_time::time_duration currentTime ( boost::posix_time::seconds( _mediaPlayer->currentTime() + _currentParameters.getOffset().total_seconds()));
|
||||
|
||||
_timeSlider->setValue( currentTime.total_seconds() );
|
||||
_curTime->setText( boost::posix_time::to_simple_string( currentTime) );
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleVolumeSliderMoved(int value)
|
||||
{
|
||||
_mediaPlayer->setVolume( value / 100. );
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleClose(void)
|
||||
{
|
||||
_close.emit();
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleParametersEdit(void)
|
||||
{
|
||||
/*
|
||||
_dialog = new VideoParametersDialog("Parameters");
|
||||
_dialog->load(_currentParameters);
|
||||
|
||||
_dialog->show();
|
||||
|
||||
_dialog->finished().connect(this, &VideoMediaPlayerWidget::handleParametersDone);
|
||||
*/
|
||||
}
|
||||
|
||||
void
|
||||
VideoMediaPlayerWidget::handleParametersDone(Wt::WDialog::DialogCode code)
|
||||
{
|
||||
assert(_dialog != nullptr);
|
||||
|
||||
if (code == Wt::WDialog::Accepted)
|
||||
{
|
||||
_dialog->save( _currentParameters );
|
||||
|
||||
// TODO SYNC current offset with player?
|
||||
// HACK use slider current value
|
||||
handlePlayOffset( _timeSlider->value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -1,85 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef __VIDEO_MEDIA_PLAYER_WIDGET_HPP
|
||||
#define __VIDEO_MEDIA_PLAYER_WIDGET_HPP
|
||||
|
||||
#include <Wt/WDialog>
|
||||
#include <Wt/WSlider>
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WContainerWidget>
|
||||
#include <Wt/WLink>
|
||||
|
||||
#include "VideoParametersDialog.hpp"
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "resource/AvConvTranscodeStreamResource.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class VideoMediaPlayerWidget : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
VideoMediaPlayerWidget( const Av::MediaFile& mediaFile, Av::TranscodeParameters parameters, Wt::WContainerWidget *parent = 0);
|
||||
|
||||
Wt::Signal<void>& close() { return _close; };
|
||||
|
||||
private:
|
||||
|
||||
// Signals
|
||||
Wt::Signal<void> _close;
|
||||
|
||||
void load(Av::TranscodeParameters parameters);
|
||||
|
||||
// Player controls
|
||||
void handlePlayOffset(int offsetSecs);
|
||||
void handleTimeUpdated(void);
|
||||
void handleSliderMoved(int value);
|
||||
void handleFullscreen(void);
|
||||
void handleVolumeSliderMoved(int value);
|
||||
|
||||
void handleClose(void);
|
||||
|
||||
void handleParametersEdit(void);
|
||||
void handleParametersDone(Wt::WDialog::DialogCode);
|
||||
|
||||
boost::filesystem::path _currentFile;
|
||||
|
||||
// Core
|
||||
Wt::WMediaPlayer* _mediaPlayer;
|
||||
AvConvTranscodeStreamResource* _mediaResource;
|
||||
Wt::WLink _mediaInternalLink;
|
||||
|
||||
// Controls
|
||||
Av::TranscodeParameters _currentParameters;
|
||||
Wt::WPushButton* _playBtn;
|
||||
Wt::WPushButton* _pauseBtn;
|
||||
Wt::WSlider* _timeSlider;
|
||||
Wt::WSlider* _volumeSlider;
|
||||
Wt::WText* _curTime;
|
||||
Wt::WText* _duration;
|
||||
|
||||
VideoParametersDialog* _dialog;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,143 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WPushButton>
|
||||
#include <Wt/WLabel>
|
||||
#include <Wt/WTable>
|
||||
|
||||
#include "VideoParametersDialog.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
static const std::list<Av::Stream::Type> streamTypes = {Av::Stream::Type::Video, Av::Stream::Type::Audio, Av::Stream::Type::Subtitle};
|
||||
|
||||
VideoParametersDialog::VideoParametersDialog(Wt::WString windowTitle, Wt::WDialog* parent)
|
||||
: Wt::WDialog(windowTitle, parent)
|
||||
{
|
||||
|
||||
Wt::WTable* layout = new Wt::WTable(contents());
|
||||
|
||||
createStreamWidgets("Video", Av::Stream::Type::Video, layout);
|
||||
createStreamWidgets("Audio", Av::Stream::Type::Audio, layout);
|
||||
createStreamWidgets("Subtitles", Av::Stream::Type::Subtitle, layout);
|
||||
|
||||
Wt::WPushButton *ok = new Wt::WPushButton("Apply", contents());
|
||||
ok->clicked().connect(this, &Wt::WDialog::accept);
|
||||
|
||||
Wt::WPushButton *cancel = new Wt::WPushButton("Cancel", contents());
|
||||
cancel->clicked().connect(this, &Wt::WDialog::reject);
|
||||
}
|
||||
|
||||
void
|
||||
VideoParametersDialog::createStreamWidgets(const Wt::WString& labelString, Av::Stream::Type type, Wt::WTable* layout)
|
||||
{
|
||||
int row = layout->rowCount();
|
||||
|
||||
Wt::WLabel* label = new Wt::WLabel(labelString);
|
||||
Wt::WComboBox *combo = new Wt::WComboBox();
|
||||
label->setBuddy(combo);
|
||||
|
||||
layout->elementAt(row, 0)->addWidget(label);
|
||||
layout->elementAt(row, 1)->addWidget(combo);
|
||||
|
||||
Wt::WStringListModel* model = new Wt::WStringListModel(combo);
|
||||
|
||||
combo->setModel(model);
|
||||
|
||||
_streamSelection.insert( std::make_pair(type, std::make_pair(combo, model) ) );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
VideoParametersDialog::handleApply()
|
||||
{
|
||||
_apply.emit();
|
||||
}
|
||||
|
||||
void
|
||||
VideoParametersDialog::addStreams(Wt::WStringListModel* model, const std::vector<Av::Stream>& streams)
|
||||
{
|
||||
for (const Av::Stream& stream : streams)
|
||||
{
|
||||
model->addString(stream.desc);
|
||||
model->setData(model->rowCount(), 0, stream.id, Wt::UserRole);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
VideoParametersDialog::selectStream(const Wt::WStringListModel* model, int streamId, Wt::WComboBox* combo)
|
||||
{
|
||||
for (int idStream = 0; idStream < model->rowCount(); ++idStream)
|
||||
{
|
||||
int id = boost::any_cast<int>( model->data( model->index(idStream, 0), Wt::UserRole));
|
||||
if (id == streamId)
|
||||
{
|
||||
combo->setCurrentIndex(idStream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
VideoParametersDialog::load(const Av::MediaFile& mediaFile, Av::TranscodeParameters currentParameters)
|
||||
{
|
||||
// Populate the combox with the available streams
|
||||
// And then show the selected one
|
||||
for (Av::Stream::Type streamType : streamTypes)
|
||||
{
|
||||
Wt::WComboBox* combo = _streamSelection[streamType].first;
|
||||
Wt::WStringListModel* model = _streamSelection[streamType].second;
|
||||
|
||||
addStreams(model, mediaFile.getStreams(streamType) );
|
||||
|
||||
// Pre Select if necessary
|
||||
std::set<int> selectedStreams = currentParameters.getSelectedStreamIds();
|
||||
for (Av::Stream stream : mediaFile.getStreams(streamType))
|
||||
{
|
||||
if (selectedStreams.find(stream.id) != selectedStreams.end())
|
||||
{
|
||||
selectStream(model, stream.id, combo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
VideoParametersDialog::save(Av::TranscodeParameters& parameters)
|
||||
{
|
||||
// Get stream selected, if any
|
||||
for (Av::Stream::Type streamType : streamTypes)
|
||||
{
|
||||
Wt::WComboBox* combo = _streamSelection[streamType].first;
|
||||
Wt::WStringListModel* model = _streamSelection[streamType].second;
|
||||
|
||||
if (combo->currentIndex() >= 0)
|
||||
{
|
||||
int streamId = boost::any_cast<int>( model->data(model->index(combo->currentIndex(), 0), Wt::UserRole));
|
||||
|
||||
parameters.addStream(streamId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,64 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <Wt/WSignal>
|
||||
#include <Wt/WDialog>
|
||||
#include <Wt/WComboBox>
|
||||
#include <Wt/WStringListModel>
|
||||
#include <Wt/WString>
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class VideoParametersDialog : public Wt::WDialog
|
||||
{
|
||||
public:
|
||||
// parameters to be edited
|
||||
VideoParametersDialog(Wt::WString windowTitle, Wt::WDialog* parent = 0);
|
||||
|
||||
// Populates widget contents using these paramaters
|
||||
void load(const Av::MediaFile& mediaFile, Av::TranscodeParameters currentParameters);
|
||||
|
||||
// Save widgets contents into these parameters
|
||||
void save(Av::TranscodeParameters& parameters);
|
||||
|
||||
// Signal to be emitted if parameters are changed
|
||||
Wt::Signal<void>& apply() { return _apply; }
|
||||
|
||||
private:
|
||||
|
||||
void handleApply(void);
|
||||
|
||||
// Stream handling
|
||||
void createStreamWidgets(const Wt::WString& label, Av::Stream::Type type, Wt::WTable* layout);
|
||||
void addStreams(Wt::WStringListModel* model, const std::vector<Av::Stream>& streams);
|
||||
void selectStream(const Wt::WStringListModel* model, int streamId, Wt::WComboBox* combo);
|
||||
|
||||
Wt::Signal<void> _apply;
|
||||
|
||||
std::map<Av::Stream::Type, std::pair<Wt::WComboBox*, Wt::WStringListModel* > > _streamSelection;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,91 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Wt/WApplication>
|
||||
#include <Wt/WEnvironment>
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "VideoWidget.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
VideoWidget::VideoWidget(Wt::WContainerWidget* parent )
|
||||
: Wt::WContainerWidget(parent)
|
||||
{
|
||||
|
||||
_videoDbWidget = new VideoDatabaseWidget(this);
|
||||
|
||||
_videoDbWidget->playVideo().connect(this, &VideoWidget::playVideo);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
VideoWidget::search(const std::string& searchText)
|
||||
{
|
||||
//TODO
|
||||
}
|
||||
|
||||
void
|
||||
VideoWidget::playVideo(boost::filesystem::path p)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Want to play video " << p << "'";
|
||||
|
||||
std::size_t audioBitrate = 0;
|
||||
std::size_t videoBitrate = 0;
|
||||
|
||||
// Get user preferences
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(DboSession());
|
||||
|
||||
audioBitrate = CurrentUser()->getMaxAudioBitrate();
|
||||
videoBitrate = CurrentUser()->getMaxVideoBitrate();
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Max bitrate set to " << videoBitrate << "/" << audioBitrate;
|
||||
|
||||
Av::MediaFile mediaFile(p);
|
||||
|
||||
if (!mediaFile.open())
|
||||
return;
|
||||
|
||||
if (!mediaFile.scan())
|
||||
return;
|
||||
|
||||
Av::TranscodeParameters parameters;
|
||||
|
||||
parameters.setEncoding( Av::Encoding::WEBMV );
|
||||
|
||||
VideoMediaPlayerWidget *mediaPlayer = new VideoMediaPlayerWidget(mediaFile, parameters, this);
|
||||
|
||||
mediaPlayer->close().connect(std::bind([=] ()
|
||||
{
|
||||
_videoDbWidget->setHidden(false);
|
||||
delete mediaPlayer;
|
||||
}));
|
||||
|
||||
_videoDbWidget->setHidden(true);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,53 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef VIDEO_WIDGET_HPP
|
||||
#define VIDEO_WIDGET_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <Wt/WContainerWidget>
|
||||
|
||||
#include "video/VideoMediaPlayerWidget.hpp"
|
||||
#include "video/VideoDatabaseWidget.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class VideoWidget : public Wt::WContainerWidget
|
||||
{
|
||||
public:
|
||||
|
||||
VideoWidget(Wt::WContainerWidget* parent = 0);
|
||||
|
||||
void search(const std::string& searchText);
|
||||
|
||||
private:
|
||||
|
||||
void backToList(void);
|
||||
void playVideo(boost::filesystem::path p);
|
||||
|
||||
VideoDatabaseWidget* _videoDbWidget;
|
||||
VideoMediaPlayerWidget* _mediaPlayer;
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user