New UI: first step

This commit is contained in:
emeric
2018-01-28 14:15:50 +01:00
parent d8f8c77c07
commit 6591006fcc
98 changed files with 1602 additions and 8515 deletions
+7 -1
View File
@@ -17,7 +17,13 @@ lms_images_DATA = \
docroot/images/unknown-artist.jpg docroot/images/unknown-artist.jpg
lms_approot_DATA = \ 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 = \ sysconf_DATA = \
$(top_srcdir)/conf/lms.conf $(top_srcdir)/conf/lms.conf
+2 -10
View File
@@ -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. It allows you to access your music and your videos using an http(s) web interface.
## Features ## Features
- Winamp-like interface, suited for large media collections - Audio transcode for maximum interoperability and low bandwith requirements
- Audio/Video transcode for maximum interoperability and low bandwith requirements
- User management - User management
- Playlist support - Playlist support
- MusicBrainzID support to handle duplicated artist and release names - 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 - Multi genre information from metadata
- High-level information from AcousticBrainz - 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. 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 ## Dependencies
### Debian ### Debian
```sh ```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 ## Build
+25
View File
@@ -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>
+25
View File
@@ -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>
+13
View File
@@ -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>
+25
View File
@@ -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>
+31
View File
@@ -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
View File
@@ -2,15 +2,23 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks--> <!--FORMS message blocks-->
<message id="Wt.WNavigationBar.template"> <message id="template-main">
<div> ${navbar-top class="main-nav"}
${collapse-button} ${contents class="container"}
<div class="navbar-header"> </message>
${expand-button}
${title-link} <message id="template-home">
${search} ${tr:msg-home-welcome}
</div> </message>
${contents}
<message id="template-explore">
${filters}
${contents}
</message>
<message id="template-filters">
<div class="form-group">
${add-filter}${filters}
</div> </div>
</message> </message>
+33
View File
@@ -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>
-32
View File
@@ -1,14 +1,5 @@
.main-nav { .main-nav {
margin-bottom: 0px; 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 { .playqueue {
@@ -198,26 +189,3 @@
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5) 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
View File
@@ -5,7 +5,7 @@ lms_SOURCES = \
$(srcdir)/av/AvInfo.cpp \ $(srcdir)/av/AvInfo.cpp \
$(srcdir)/av/AvTranscoder.cpp \ $(srcdir)/av/AvTranscoder.cpp \
$(srcdir)/cover/CoverArtGrabber.cpp \ $(srcdir)/cover/CoverArtGrabber.cpp \
$(srcdir)/database/Artist.cpp \ $(srcdir)/database/DbArtist.cpp \
$(srcdir)/database/DatabaseHandler.cpp \ $(srcdir)/database/DatabaseHandler.cpp \
$(srcdir)/database/MediaDirectory.cpp \ $(srcdir)/database/MediaDirectory.cpp \
$(srcdir)/database/Playlist.cpp \ $(srcdir)/database/Playlist.cpp \
@@ -24,41 +24,22 @@ lms_SOURCES = \
$(srcdir)/image/Image.cpp \ $(srcdir)/image/Image.cpp \
$(srcdir)/metadata/AvFormat.cpp \ $(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/metadata/TagLibParser.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/LmsApplication.cpp \
$(srcdir)/ui/ReleasesView.cpp \
$(srcdir)/ui/ReleaseView.cpp \
$(srcdir)/ui/TracksView.cpp \
$(srcdir)/ui/auth/LmsAuth.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/DirectoryValidator.cpp \
$(srcdir)/ui/common/InputRange.cpp \ $(srcdir)/ui/common/InputRange.cpp \
$(srcdir)/ui/common/LineEdit.cpp \ $(srcdir)/ui/common/LineEdit.cpp \
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
$(srcdir)/ui/resource/ImageResource.cpp \ $(srcdir)/ui/resource/ImageResource.cpp \
$(srcdir)/ui/resource/TranscodeResource.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/Config.cpp \
$(srcdir)/utils/Logger.cpp \ $(srcdir)/utils/Logger.cpp \
$(srcdir)/utils/Path.cpp \ $(srcdir)/utils/Path.cpp \
+1 -5
View File
@@ -178,11 +178,7 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re
if (!release || release->isNone()) if (!release || release->isNone())
return std::vector<Image::Image>(); return std::vector<Image::Image>();
// Get the first track of the release std::vector<Track::pointer> tracks = release->getTracks();
std::vector<Track::pointer> tracks = Track::getByFilter(session,
SearchFilter::ById(SearchFilter::Field::Release, releaseId),
-1, 1 /* limit result size */);
if (tracks.empty()) if (tracks.empty())
return std::vector<Image::Image>(); return std::vector<Image::Image>();
+1 -1
View File
@@ -187,7 +187,7 @@ Handler::createConnectionPool(boost::filesystem::path p)
connection->executeSql("pragma journal_mode=WAL"); connection->executeSql("pragma journal_mode=WAL");
// connection->setProperty("show-queries", "true"); connection->setProperty("show-queries", "true");
return new Wt::Dbo::FixedSqlConnectionPool(connection, 1); return new Wt::Dbo::FixedSqlConnectionPool(connection, 1);
} }
+1
View File
@@ -33,6 +33,7 @@
namespace Database { namespace Database {
typedef Wt::Dbo::dbo_default_traits::IdType id_type;
typedef Wt::Auth::Dbo::UserDatabase<AuthInfo> UserDatabase; typedef Wt::Auth::Dbo::UserDatabase<AuthInfo> UserDatabase;
// Session living class handling the database and the login // Session living class handling the database and the login
+202
View File
@@ -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
+98
View File
@@ -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
View File
@@ -82,68 +82,61 @@ Release::getAll(Wt::Dbo::Session& session, int offset, int size)
std::vector<Release::pointer> std::vector<Release::pointer>
Release::getAllOrphans(Wt::Dbo::Session& session) 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()); return std::vector<pointer>(res.begin(), res.end());
} }
static
Wt::Dbo::Query<Release::pointer> 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 std::ostringstream oss;
= 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"); oss << "SELECT DISTINCT r FROM release r";
for (const std::string& bindArg : sqlQuery.where().getBindArgs()) for (auto keyword : keywords)
query.bind(bindArg); where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
return query; if (!clusterIds.empty())
}
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)
{ {
model.addColumn( "r.name", columnNames[0]); 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";
model.addColumn( "t.date", columnNames[1]);
model.addColumn( "COUNT(DISTINCT t.id)", columnNames[2]); 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> 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()); auto res = std::vector<pointer>(collection.begin(), collection.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);
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1) 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; return res;
} }
int boost::optional<int>
Release::getReleaseYear(bool original) const Release::getReleaseYear(bool original) const
{ {
assert(session()); assert(session());
// TODO something better Wt::Dbo::collection<boost::posix_time::ptime> times = session()->query<boost::posix_time::ptime>(
auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1); 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()) /* various dates, no date */
return 0; if (times.empty() || times.size() > 1)
return boost::none;
boost::gregorian::date date; boost::gregorian::date date = times.front().date();
if (original)
date = tracks.front()->getOriginalDate().date();
else
date = tracks.front()->getDate().date();
if (date.is_special()) 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 } // namespace Database
+20 -18
View File
@@ -17,11 +17,11 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef _DB_RELEASE_HPP_ #pragma once
#define _DB_RELEASE_HPP_
#include <boost/optional.hpp>
#include <Wt/Dbo/Dbo> #include <Wt/Dbo/Dbo>
#include <Wt/Dbo/QueryModel>
#include "SearchFilter.hpp" #include "SearchFilter.hpp"
@@ -30,6 +30,7 @@ namespace Database
class Track; class Track;
class Release; class Release;
class Artist;
class Release : public Wt::Dbo::Dbo<Release> 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 std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
static pointer getById(Wt::Dbo::Session& session, id_type id); static pointer getById(Wt::Dbo::Session& session, id_type id);
static pointer getNone(Wt::Dbo::Session& session); // Special entry 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> 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 // Create
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = ""); static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
// Utility functions // 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 // Accessors
// 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
std::string getName() const { return _name; } std::string getName() const { return _name; }
std::string getMBID() const { return _MBID; } std::string getMBID() const { return _MBID; }
bool isNone(void) const; bool isNone(void) const;
boost::posix_time::time_duration getDuration(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; } void setMBID(std::string mbid) { _MBID = mbid; }
template<class Action> template<class Action>
@@ -82,8 +87,6 @@ class Release : public Wt::Dbo::Dbo<Release>
} }
private: private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128; static const std::size_t _maxNameLength = 128;
std::string _name; std::string _name;
@@ -94,5 +97,4 @@ class Release : public Wt::Dbo::Dbo<Release>
} // namespace Database } // namespace Database
#endif
+4 -87
View File
@@ -24,98 +24,15 @@
namespace Database 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; 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; return sqlQuery;
} }
+14 -48
View File
@@ -31,71 +31,37 @@
namespace Database namespace Database
{ {
typedef Wt::Dbo::dbo_default_traits::IdType id_type;
class SearchFilter class SearchFilter
{ {
public: public:
enum class Field { typedef int id_type;
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;
SearchFilter() {} SearchFilter() {}
// Helpers static SearchFilter Artist(std::string name) {return SearchFilter();}
static SearchFilter IdMatch( const IdMatchType& _idMatch ) static SearchFilter Artist(id_type id) {return SearchFilter();}
{
return SearchFilter(_idMatch);
}
static SearchFilter NameLikeMatch( const NameLikeMatchType& _nameLikeMatch ) static SearchFilter Release(std::string name) {return SearchFilter();}
{ static SearchFilter Release(id_type id) {return SearchFilter();}
return SearchFilter(_nameLikeMatch);
}
// Single Field ID match static SearchFilter Track(std::string name) {return SearchFilter();}
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
{
return SearchFilter({{field, {id} }});
}
// Single Field Name match OR static SearchFilter Cluster(id_type id) {return SearchFilter();}
static SearchFilter ByNameOr(Field field, std::vector<std::string> keywords)
{
return NameLikeMatch({{{field, keywords}}});
}
// Single Field Name match AND // Combine search filters by add operation
static SearchFilter ByNameAnd(Field field, std::vector<std::string> keywords) // Caution: multiple filters on different artist/release/track
{ // values may lead to empty results
NameLikeMatchType nameLikeMatch; void operator+=(const SearchFilter& filter);
for (auto keyword : keywords) SqlQuery generatePartialQuery();
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;
private: private:
SearchFilter(const NameLikeMatchType& _nameLikeMatch) : nameLikeMatch(_nameLikeMatch) {}
SearchFilter(const IdMatchType& _idMatch) : idMatch(_idMatch) {}
}; };
SqlQuery generatePartialQuery(SearchFilter& filter);
} // namespace Database } // namespace Database
+62 -71
View File
@@ -108,29 +108,42 @@ Track::getClusters(void) const
return clusters; return clusters;
} }
static
Wt::Dbo::Query< Track::pointer > 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 std::ostringstream oss;
= 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"); oss << "SELECT t FROM track t";
for (const std::string& bindArg : sqlQuery.where().getBindArgs()) for (auto keyword : keywords)
query.bind(bindArg); 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 > WhereClause clusterClause;
Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<UIQueryResult> query for (auto id : clusterIds)
= 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"); 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); query.bind(bindArg);
return query; return query;
@@ -139,7 +152,7 @@ Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
Track::StatsQueryResult Track::StatsQueryResult
Track::getStats(Wt::Dbo::Session& session, SearchFilter filter) 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)"); 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> 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()); auto res = std::vector<pointer>(collection.begin(), collection.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);
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1) 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; return res;
} }
void boost::optional<std::size_t>
Track::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames) Track::getTrackNumber(void) const
{ {
Wt::Dbo::Query< UIQueryResult > query = getUIQuery(session, filter); return (_trackNumber > 0) ? boost::make_optional<std::size_t>(_trackNumber) : boost::none;
model.setQuery(query, columnNames.empty() ? true : false); }
// TODO do something better boost::optional<std::size_t>
if (columnNames.size() == 9) Track::getTotalTrackNumber(void) const
{ {
model.addColumn( "a.name", columnNames[0] ); return (_totalTrackNumber > 0) ? boost::make_optional<std::size_t>(_totalTrackNumber) : boost::none;
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::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)); 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::pointer
Cluster::getNone(Wt::Dbo::Session& session) Cluster::getNone(Wt::Dbo::Session& session)
{ {
@@ -252,7 +271,7 @@ Cluster::remove(Wt::Dbo::Session& session, std::string type)
Wt::Dbo::Query<Cluster::pointer> Wt::Dbo::Query<Cluster::pointer>
Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter) Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
{ {
SqlQuery sqlQuery = generatePartialQuery(filter); SqlQuery sqlQuery = filter.generatePartialQuery();
Wt::Dbo::Query<pointer> query 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"); = 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; 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> std::vector<Cluster::pointer>
Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
{ {
+18 -30
View File
@@ -24,6 +24,7 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
#include <boost/optional.hpp>
#include <Wt/Dbo/Dbo> #include <Wt/Dbo/Dbo>
#include <Wt/Dbo/WtSqlTraits> #include <Wt/Dbo/WtSqlTraits>
@@ -44,6 +45,12 @@ class Cluster
{ {
public: public:
enum class Type
{
Genre = 1,
Mood = 2,
};
typedef Wt::Dbo::ptr<Cluster> pointer; typedef Wt::Dbo::ptr<Cluster> pointer;
typedef Wt::Dbo::dbo_traits<Cluster>::IdType id_type; 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 Wt::Dbo::collection<pointer> getAll(Wt::Dbo::Session& session);
static std::vector<pointer> getByType(Wt::Dbo::Session& session, std::string type); 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 // Create utility
static pointer create(Wt::Dbo::Session& session, std::string type, std::string name); 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 getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p);
static pointer getById(Wt::Dbo::Session& session, id_type id); static pointer getById(Wt::Dbo::Session& session, id_type id);
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID); 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,
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool &moreResults); 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 Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session);
static std::vector<id_type> getAllIds(Wt::Dbo::Session& session); // nested transaction 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 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); static std::vector<pointer> getChecksumDuplicates(Wt::Dbo::Session& session);
// Utility fonctions // 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 // Stats for a given search filter
typedef boost::tuple< typedef boost::tuple<
int, // Total tracks int, // Total tracks
@@ -174,10 +164,10 @@ class Track
void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; } void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; }
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; } void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
int getTrackNumber(void) const { return _trackNumber; } boost::optional<std::size_t> getTrackNumber(void) const;
int getTotalTrackNumber(void) const { return _totalTrackNumber; } boost::optional<std::size_t> getTotalTrackNumber(void) const;
int getDiscNumber(void) const { return _discNumber; } boost::optional<std::size_t> getDiscNumber(void) const;
int getTotalDiscNumber(void) const { return _totalDiscNumber; } boost::optional<std::size_t> getTotalDiscNumber(void) const;
std::string getName(void) const { return _name; } std::string getName(void) const { return _name; }
boost::filesystem::path getPath(void) const { return _filePath; } boost::filesystem::path getPath(void) const { return _filePath; }
boost::posix_time::time_duration getDuration(void) const { return _duration; } boost::posix_time::time_duration getDuration(void) const { return _duration; }
@@ -218,8 +208,6 @@ class Track
private: private:
static Wt::Dbo::Query< pointer > getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128; static const std::size_t _maxNameLength = 128;
int _trackNumber; int _trackNumber;
+1 -1
View File
@@ -19,7 +19,7 @@
// header file aimed to ease database class declarations // header file aimed to ease database class declarations
#include "Artist.hpp" #include "DbArtist.hpp"
#include "Track.hpp" #include "Track.hpp"
#include "Playlist.hpp" #include "Playlist.hpp"
#include "Release.hpp" #include "Release.hpp"
+122
View File
@@ -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. * This file is part of LMS.
* *
@@ -20,19 +20,21 @@
#pragma once #pragma once
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
#include <Wt/WSignal>
#include "Filters.hpp"
namespace UserInterface { namespace UserInterface {
namespace Mobile {
class TrackSearchView : public Wt::WContainerWidget class Artist : public Wt::WContainerWidget
{ {
public: public:
Artist(Filters* filters, Wt::WContainerWidget* parent = 0);
TrackSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent = 0); private:
void refresh();
Filters* _filters;
}; };
} // namespace Mobile
} // namespace UserInterface } // namespace UserInterface
+92
View File
@@ -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. * This file is part of LMS.
* *
@@ -21,27 +21,21 @@
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
#include "audio/Audio.hpp" #include "Filters.hpp"
#include "MobilePlayQueueEvents.hpp"
namespace UserInterface { namespace UserInterface {
namespace Mobile {
class Audio : public UserInterface::Audio class Artists : public Wt::WContainerWidget
{ {
public: public:
Audio(Wt::WContainerWidget *parent = 0); Artists(Filters* filters, Wt::WContainerWidget* parent = 0);
void search(std::string text);
private: private:
void refresh(std::vector<std::string> searchKeywords = std::vector<std::string>());
PlayQueueEvents _playQueueEvents; Filters* _filters;
Wt::WContainerWidget* _artistsContainer;
}; };
} // namespace Mobile
} // namespace UserInterface } // namespace UserInterface
+98
View File
@@ -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. * This file is part of LMS.
* *
@@ -22,16 +22,12 @@
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
namespace UserInterface { namespace UserInterface {
namespace Mobile {
class ArtistSearchView : public Wt::WContainerWidget class Explore : public Wt::WContainerWidget
{ {
public: public:
Explore(Wt::WContainerWidget *parent = 0);
ArtistSearchView(Wt::WContainerWidget* parent = 0);
}; };
} // namespace Mobile
} // namespace UserInterface } // namespace UserInterface
+16 -12
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Emeric Poupon * Copyright (C) 2018 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,25 +17,29 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef UI_AUDIO_HPP #include <Wt/WTemplate>
#define UI_AUDIO_HPP #include <Wt/WPushButton>
#include <string> #include "Filters.hpp"
#include <Wt/WContainerWidget>
namespace UserInterface { 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) {} // Filters
virtual ~Audio() {} 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 } // namespace UserInterface
#endif
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2015 Emeric Poupon * Copyright (C) 2018 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -20,24 +20,25 @@
#pragma once #pragma once
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
#include <Wt/WLink>
#include "database/Types.hpp" #include "database/Types.hpp"
namespace UserInterface { #include "Filters.hpp"
namespace Mobile {
class ArtistView : public Wt::WContainerWidget namespace UserInterface {
class Filters : public Wt::WContainerWidget
{ {
public: 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 UserInterface
} //namespace Mobile
} //namespace UserInterface
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2016 Emeric Poupon * Copyright (C) 2018 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,20 +17,22 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#pragma once #include <Wt/WTemplate>
#include <Wt/WSignal> #include "utils/Logger.hpp"
#include "database/Types.hpp"
#include "LmsApplication.hpp"
#include "HomeView.hpp"
namespace UserInterface { namespace UserInterface {
namespace Mobile {
struct PlayQueueEvents
Home::Home(Wt::WContainerWidget *parent)
: Wt::WContainerWidget(parent)
{ {
Wt::Signal<Database::Track::id_type> trackPlay; addWidget(new Wt::WTemplate(Wt::WString::tr("template-home")));
Wt::Signal<Database::Track::id_type> trackAdd; }
};
} // 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. * This file is part of LMS.
* *
@@ -22,16 +22,18 @@
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
namespace UserInterface { namespace UserInterface {
namespace Mobile {
class ReleaseSearchView : public Wt::WContainerWidget class Home : public Wt::WContainerWidget
{ {
public: public:
Home(Wt::WContainerWidget *parent = 0);
ReleaseSearchView(Wt::WContainerWidget* parent = 0); private:
void displayFilter(void);
void displayContents(void);
}; };
} // namespace Mobile
} // namespace UserInterface } // namespace UserInterface
+111 -92
View File
@@ -19,28 +19,19 @@
#include <Wt/WEnvironment> #include <Wt/WEnvironment>
#include <Wt/WBootstrapTheme> #include <Wt/WBootstrapTheme>
#include <Wt/WVBoxLayout>
#include <Wt/WNavigationBar> #include <Wt/WNavigationBar>
#include <Wt/WStackedWidget> #include <Wt/WStackedWidget>
#include <Wt/WMenu> #include <Wt/WMenu>
#include <Wt/WPopupMenu> #include <Wt/WText>
#include <Wt/WVBoxLayout>
#include <Wt/Auth/Identity>
#include "config/config.h" #include "config/config.h"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
#include "settings/Settings.hpp"
#include "settings/SettingsFirstConnectionFormView.hpp"
#include "auth/LmsAuth.hpp" #include "auth/LmsAuth.hpp"
#include "audio/desktop/DesktopAudio.hpp" #include "Explore.hpp"
#include "audio/mobile/MobileAudio.hpp" #include "HomeView.hpp"
#if HAVE_VIDEO
#include "video/VideoWidget.hpp"
#endif
#include "common/LineEdit.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -48,19 +39,6 @@ namespace skeletons {
extern const char *AuthStrings_xml1; 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 { namespace UserInterface {
Wt::WApplication* Wt::WApplication*
@@ -100,7 +78,13 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
useStyleSheet("resources/font-awesome/css/font-awesome.min.css"); useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
// Add a resource bundle // Add a resource bundle
messageResourceBundle().use(appRoot() + "artist");
messageResourceBundle().use(appRoot() + "artists");
messageResourceBundle().use(appRoot() + "messages");
messageResourceBundle().use(appRoot() + "templates"); messageResourceBundle().use(appRoot() + "templates");
messageResourceBundle().use(appRoot() + "release");
messageResourceBundle().use(appRoot() + "releases");
messageResourceBundle().use(appRoot() + "tracks");
setTitle("LMS"); 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; 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 here is no account in the database, launch the first connection wizard
if (firstConnection) // if (firstConnection)
createFirstConnectionUI(); // createFirstConnectionUI();
else // else
createLmsUI(); createLmsUI();
} }
@@ -149,13 +133,19 @@ TranscodeResource* SessionTranscodeResource()
return LmsApplication::instance()->getTranscodeResource(); return LmsApplication::instance()->getTranscodeResource();
} }
void
LmsApplication::goHome()
{
setInternalPath("/home", true);
}
void void
LmsApplication::createFirstConnectionUI() LmsApplication::createFirstConnectionUI()
{ {
// Hack, use the auth widget builtin strings // Hack, use the auth widget builtin strings
builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1); builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1);
root()->addWidget( new Settings::FirstConnectionFormView()); // root()->addWidget( new Settings::FirstConnectionFormView());
} }
void void
@@ -164,6 +154,8 @@ LmsApplication::createLmsUI()
_imageResource = new ImageResource(_db, root()); _imageResource = new ImageResource(_db, root());
_transcodeResource = new TranscodeResource(_db, root()); _transcodeResource = new TranscodeResource(_db, root());
handleAuthEvent();
/*
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent); DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
LmsAuth *authWidget = new LmsAuth(); LmsAuth *authWidget = new LmsAuth();
@@ -174,11 +166,53 @@ LmsApplication::createLmsUI()
authWidget->processEnvironment(); authWidget->processEnvironment();
root()->addWidget(authWidget); 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 void
LmsApplication::handleAuthEvent(void) LmsApplication::handleAuthEvent(void)
{ {
/*
if (!DbHandler().getLogin().loggedIn()) if (!DbHandler().getLogin().loggedIn())
{ {
LMS_LOG(UI, INFO) << "User logged out, session = " << Wt::WApplication::instance()->sessionId(); 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(); 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); auto main = new Wt::WTemplate(Wt::WString::tr("template-main"), root());
setConfirmCloseMessage("Closing LMS. Are you sure?");
// Create a Vertical layout: top is the nav bar, bottom is the contents // Navbar
Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root()); 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(); main->bindWidget("navbar-top", navbar);
navigation->setTitle("LMS", "https://github.com/epoupon/lms");
navigation->setResponsive(true);
navigation->addStyleClass("main-nav");
Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget(); auto menu = new Wt::WMenu();
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)
{ {
if (item && item->text() == "Logout") auto menuItem = menu->insertItem(0, Wt::WString::tr("msg-artists"));
DbHandler().getLogin().logout(); menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/artists"));
}, std::placeholders::_1)); menuItem->setSelectable(false);
}
searchEdit->timedChanged().connect(std::bind([=] ()
{ {
// TODO: check which view is activated and search into it auto menuItem = menu->insertItem(1, Wt::WString::tr("msg-releases"));
audio->search(searchEdit->text().toUTF8()); 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); handlePathChange(mainStack);
layout->addWidget(contentsStack, 1);
layout->setContentsMargins(0, 0, 0, 0);
if (agentIsMobile())
setInternalPath("/audio/search/preview", true);
else
setInternalPath("/audio");
} }
} // namespace UserInterface } // namespace UserInterface
+4
View File
@@ -44,6 +44,9 @@ class LmsApplication : public Wt::WApplication
TranscodeResource* getTranscodeResource() { return _transcodeResource; } TranscodeResource* getTranscodeResource() { return _transcodeResource; }
Database::Handler& getDbHandler() { return _db;} Database::Handler& getDbHandler() { return _db;}
// Utils
void goHome();
private: private:
void handleAuthEvent(void); void handleAuthEvent(void);
@@ -67,6 +70,7 @@ Database::User::pointer CurrentUser();
ImageResource *SessionImageResource(); ImageResource *SessionImageResource();
TranscodeResource *SessionTranscodeResource(); TranscodeResource *SessionTranscodeResource();
} // namespace UserInterface } // namespace UserInterface
#endif #endif
+151
View File
@@ -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. * This file is part of LMS.
* *
@@ -17,18 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#pragma once
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
namespace UserInterface { namespace UserInterface {
namespace Settings {
class Settings : public Wt::WContainerWidget class Release : public Wt::WContainerWidget
{ {
public: public:
Settings(Wt::WContainerWidget* parent = 0); Release(Wt::WContainerWidget* parent = 0);
private: private:
void refresh();
}; };
} // namespace Settings
} // namespace UserInterface } // namespace UserInterface
+103
View File
@@ -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
+38
View File
@@ -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
+109
View File
@@ -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. * This file is part of LMS.
* *
@@ -21,19 +21,18 @@
#include <Wt/WContainerWidget> #include <Wt/WContainerWidget>
#include "MobilePlayQueueEvents.hpp"
namespace UserInterface { namespace UserInterface {
namespace Mobile {
class PreviewSearchView : public Wt::WContainerWidget class Tracks : public Wt::WContainerWidget
{ {
public: 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 } // namespace UserInterface
-357
View File
@@ -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
-76
View File
@@ -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
-556
View File
@@ -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
-81
View File
@@ -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
-57
View File
@@ -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
-75
View File
@@ -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
-59
View File
@@ -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
-634
View File
@@ -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
-92
View File
@@ -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
-282
View File
@@ -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
-112
View File
@@ -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
-234
View File
@@ -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
-80
View File
@@ -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
-104
View File
@@ -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
-59
View File
@@ -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
-61
View File
@@ -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
-66
View File
@@ -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
-158
View File
@@ -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
-206
View File
@@ -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
-75
View File
@@ -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
-88
View File
@@ -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
-135
View File
@@ -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
-58
View File
@@ -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
-58
View File
@@ -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
-214
View File
@@ -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
-66
View File
@@ -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
-32
View File
@@ -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");
}
-123
View File
@@ -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
-59
View File
@@ -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
-61
View File
@@ -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
-110
View File
@@ -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
-285
View File
@@ -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
-258
View File
@@ -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
-49
View File
@@ -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
-411
View File
@@ -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
-55
View File
@@ -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
-202
View File
@@ -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
-52
View File
@@ -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
-143
View File
@@ -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
-52
View File
@@ -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
-200
View File
@@ -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
-85
View File
@@ -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
-143
View File
@@ -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
-64
View File
@@ -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
-91
View File
@@ -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
-53
View File
@@ -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