New UI: first step
This commit is contained in:
+7
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="template-artist">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>${name}</h2>
|
||||||
|
</div>
|
||||||
|
${releases class="list-group"}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="template-artist-entry">
|
||||||
|
<div class="media list-group-item">
|
||||||
|
<div class="pull-left media-middle">
|
||||||
|
${cover class="media-object"}
|
||||||
|
</div>
|
||||||
|
<div class="media-body">
|
||||||
|
<h4 class="media-heading">${name}</h4>
|
||||||
|
</div>
|
||||||
|
${<if-has-various-artists>}${tr:msg-various-artists}${</if-has-various-artists>}
|
||||||
|
<h4>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</h4>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="template-artists">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>${tr:msg-artists}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||||
|
${search}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${artists class="list-group"}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="template-artists-entry">
|
||||||
|
<div class="list-group-item">
|
||||||
|
<span class="badge">${nb-release}</span>
|
||||||
|
${name}
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="msg-various-artists">Various artists</message>
|
||||||
|
<message id="msg-artists">Artists</message>
|
||||||
|
<message id="msg-releases">Albums</message>
|
||||||
|
<message id="msg-tracks">Tracks</message>
|
||||||
|
<message id="msg-playlist">Playlist</message>
|
||||||
|
<message id="msg-filter-add">Add filter</message>
|
||||||
|
<message id="msg-search-placeholder">Search...</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="template-release">
|
||||||
|
${cover}
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>${name}</h2>
|
||||||
|
<h4>${artist-name}</h4>
|
||||||
|
<h4>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</h4>
|
||||||
|
${play-btn}${add-btn}
|
||||||
|
</div>
|
||||||
|
${tracks}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="template-release-entry">
|
||||||
|
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}. ${</if-has-track-number>}
|
||||||
|
${<if-has-artist>}${artist-name} - ${</if-has-artist>}
|
||||||
|
${name}
|
||||||
|
${play-btn}${add-btn}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
|
||||||
|
</messages>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="template-releases">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>${tr:msg-releases}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||||
|
${search}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${releases class="list-group"}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="template-releases-entry">
|
||||||
|
<div class="media list-group-item">
|
||||||
|
<div class="pull-left media-middle">
|
||||||
|
${cover class="media-object"}
|
||||||
|
</div>
|
||||||
|
<div class="media-body">
|
||||||
|
<h4 class="media-heading">${release-name}</h4>
|
||||||
|
${artist-name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
+17
-9
@@ -2,15 +2,23 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
|
<!--FORMS message blocks-->
|
||||||
|
|
||||||
|
<message id="template-tracks">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>${tr:msg-tracks}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||||
|
${search}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${tracks class="list-group"}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="template-tracks-entry">
|
||||||
|
<div class="media list-group-item">
|
||||||
|
<div class="pull-left media-middle">
|
||||||
|
${cover class="media-object"}
|
||||||
|
</div>
|
||||||
|
<div class="media-body">
|
||||||
|
<h4 class="media-heading">${name}</h4>
|
||||||
|
</div>
|
||||||
|
${<if-has-release>}${release-name}${</if-has-release>}
|
||||||
|
${<if-has-artist>}${artist-name}${</if-has-artist>}
|
||||||
|
${play-btn}${add-btn}
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</messages>
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
.main-nav {
|
.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
@@ -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 \
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Types.hpp"
|
||||||
|
#include "SqlQuery.hpp"
|
||||||
|
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
|
||||||
|
Artist::Artist(const std::string& name, const std::string& MBID)
|
||||||
|
: _name(std::string(name, 0 , _maxNameLength)),
|
||||||
|
_MBID(MBID)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getByName(Wt::Dbo::Session& session, const std::string& name)
|
||||||
|
{
|
||||||
|
Wt::Dbo::collection<Artist::pointer> res = session.find<Artist>().where("name = ?").bind( std::string(name, 0, _maxNameLength) );
|
||||||
|
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
Artist::pointer
|
||||||
|
Artist::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
|
||||||
|
{
|
||||||
|
return session.find<Artist>().where("mbid = ?").bind(mbid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Artist::pointer
|
||||||
|
Artist::getById(Wt::Dbo::Session& session, Artist::id_type id)
|
||||||
|
{
|
||||||
|
return session.find<Artist>().where("id = ?").bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Artist::pointer
|
||||||
|
Artist::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
|
||||||
|
{
|
||||||
|
return session.add(new Artist(name, MBID));
|
||||||
|
}
|
||||||
|
|
||||||
|
Artist::pointer
|
||||||
|
Artist::getNone(Wt::Dbo::Session& session)
|
||||||
|
{
|
||||||
|
std::vector<pointer> res = getByName(session, "<None>");
|
||||||
|
if (res.empty())
|
||||||
|
return create(session, "<None>");
|
||||||
|
|
||||||
|
return res.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Artist::isNone() const
|
||||||
|
{
|
||||||
|
return _name == "<None>";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getAll(Wt::Dbo::Session& session, int offset, int size)
|
||||||
|
{
|
||||||
|
Wt::Dbo::collection<pointer> res = session.find<Artist>().orderBy("LOWER(name)").offset(offset).limit(size);
|
||||||
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getAllOrphans(Wt::Dbo::Session& session)
|
||||||
|
{
|
||||||
|
Wt::Dbo::collection<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("SELECT DISTINCT a FROM artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL");
|
||||||
|
|
||||||
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
Wt::Dbo::Query<Artist::pointer>
|
||||||
|
getQuery(Wt::Dbo::Session& session,
|
||||||
|
const std::vector<id_type>& clusterIds,
|
||||||
|
const std::vector<std::string>& keywords)
|
||||||
|
{
|
||||||
|
WhereClause where;
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "SELECT DISTINCT a FROM artist a";
|
||||||
|
|
||||||
|
for (auto keyword : keywords)
|
||||||
|
where.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
oss << " INNER JOIN track t ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||||
|
|
||||||
|
where.And(clusterClause);
|
||||||
|
}
|
||||||
|
oss << " " << where.get();
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||||
|
|
||||||
|
oss << " ORDER BY a.name";
|
||||||
|
|
||||||
|
Wt::Dbo::Query<Artist::pointer> query = session.query<Artist::pointer>( oss.str() );
|
||||||
|
|
||||||
|
for (const std::string& bindArg : where.getBindArgs())
|
||||||
|
{
|
||||||
|
query.bind(bindArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
Artist::getByFilter(Wt::Dbo::Session& session,
|
||||||
|
const std::vector<id_type>& clusters,
|
||||||
|
const std::vector<std::string> keywords,
|
||||||
|
int offset, int size, bool& moreResults)
|
||||||
|
{
|
||||||
|
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords).limit(size).offset(offset);
|
||||||
|
|
||||||
|
auto res = std::vector<pointer>(collection.begin(), collection.end());
|
||||||
|
|
||||||
|
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Wt::Dbo::ptr<Release> >
|
||||||
|
Artist::getReleases(const std::vector<id_type>& clusterIds) const
|
||||||
|
{
|
||||||
|
assert(self());
|
||||||
|
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
WhereClause where;
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id";
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||||
|
|
||||||
|
where.And(clusterClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
where.And(WhereClause("a.id = ?")).bind(std::to_string(id()));
|
||||||
|
|
||||||
|
oss << " " << where.get();
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||||
|
|
||||||
|
// TODO order
|
||||||
|
oss << " ORDER BY t.date,r.name";
|
||||||
|
|
||||||
|
Wt::Dbo::Query<Release::pointer> query = session()->query<Release::pointer>( oss.str() );
|
||||||
|
|
||||||
|
for (const std::string& bindArg : where.getBindArgs())
|
||||||
|
{
|
||||||
|
query.bind(bindArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::collection< Wt::Dbo::ptr<Release> > res = query;
|
||||||
|
|
||||||
|
return std::vector< Wt::Dbo::ptr<Release> > (res.begin(), res.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace Database
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _DB_ARTIST_HPP_
|
||||||
|
#define _DB_ARTIST_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Dbo>
|
||||||
|
#include <Wt/Dbo/QueryModel>
|
||||||
|
|
||||||
|
#include "SearchFilter.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
|
||||||
|
class Track;
|
||||||
|
class Cluster;
|
||||||
|
class Release;
|
||||||
|
|
||||||
|
class Artist : public Wt::Dbo::Dbo<Artist>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
typedef Wt::Dbo::ptr<Artist> pointer;
|
||||||
|
typedef Wt::Dbo::dbo_traits<Artist>::IdType id_type;
|
||||||
|
|
||||||
|
Artist() {}
|
||||||
|
Artist(const std::string& name, const std::string& MBID = "");
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
|
||||||
|
static pointer getById(Wt::Dbo::Session& session, id_type id);
|
||||||
|
static pointer getNone(Wt::Dbo::Session& session); // Special entry
|
||||||
|
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
|
||||||
|
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
|
||||||
|
const std::vector<id_type>& clusters, // at least one track that belongs to these clusters
|
||||||
|
const std::vector<std::string> keywords, // name must match all of these keywords
|
||||||
|
int offset,
|
||||||
|
int size,
|
||||||
|
bool& moreExpected);
|
||||||
|
|
||||||
|
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1);
|
||||||
|
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // No track related
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
std::string getName(void) const { return _name; }
|
||||||
|
std::string getMBID(void) const { return _MBID; }
|
||||||
|
|
||||||
|
// Get the releases that have at least one track for this artist + belongs to optonal cluster filters
|
||||||
|
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::vector<id_type>& clusterIds = std::vector<id_type>()) const;
|
||||||
|
|
||||||
|
void setMBID(std::string mbid) { _MBID = mbid; }
|
||||||
|
|
||||||
|
// Create
|
||||||
|
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
|
||||||
|
|
||||||
|
bool isNone(void) const;
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
Wt::Dbo::field(a, _MBID, "mbid");
|
||||||
|
|
||||||
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
static const std::size_t _maxNameLength = 128;
|
||||||
|
|
||||||
|
std::string _name;
|
||||||
|
std::string _MBID; // Musicbrainz Identifier
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks of this artist
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
|
#endif
|
||||||
+87
-59
@@ -82,68 +82,61 @@ Release::getAll(Wt::Dbo::Session& session, int offset, int size)
|
|||||||
std::vector<Release::pointer>
|
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
@@ -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
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Wt/WApplication>
|
||||||
|
#include <Wt/WAnchor>
|
||||||
|
#include <Wt/WLineEdit>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
#include <Wt/WText>
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
#include "ReleasesView.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Releases::Releases(Wt::WContainerWidget* parent)
|
||||||
|
: Wt::WContainerWidget(parent)
|
||||||
|
{
|
||||||
|
auto releases = new Wt::WTemplate(Wt::WString::tr("template-releases"), this);
|
||||||
|
releases->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
auto search = new Wt::WLineEdit();
|
||||||
|
releases->bindWidget("search", search);
|
||||||
|
search->setPlaceholderText(Wt::WString::tr("msg-search-placeholder"));
|
||||||
|
search->textInput().connect(std::bind([this, search]
|
||||||
|
{
|
||||||
|
auto keywords = splitString(search->text().toUTF8(), " ");
|
||||||
|
refresh(keywords);
|
||||||
|
}));
|
||||||
|
|
||||||
|
_releasesContainer = new Wt::WContainerWidget();
|
||||||
|
releases->bindWidget("releases", _releasesContainer);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Releases::refresh(std::vector<std::string> searchKeywords)
|
||||||
|
{
|
||||||
|
_releasesContainer->clear();
|
||||||
|
|
||||||
|
std::vector<Cluster::id_type> clusterIds; // TODO fill
|
||||||
|
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
bool moreResults;
|
||||||
|
auto releases = Release::getByFilter(DboSession(), clusterIds, searchKeywords, 0, 40, moreResults);
|
||||||
|
|
||||||
|
for (auto release : releases)
|
||||||
|
{
|
||||||
|
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-releases-entry"), _releasesContainer);
|
||||||
|
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(release.id())));
|
||||||
|
Wt::WImage *cover = new Wt::WImage(coverAnchor);
|
||||||
|
cover->setImageLink(SessionImageResource()->getReleaseUrl(release.id(), 128));
|
||||||
|
// Some images may not be square
|
||||||
|
cover->setWidth(128);
|
||||||
|
entry->bindWidget("cover", coverAnchor);
|
||||||
|
|
||||||
|
entry->bindString("release-name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
||||||
|
|
||||||
|
auto artists = release->getArtists();
|
||||||
|
if (artists.size() > 1)
|
||||||
|
{
|
||||||
|
entry->bindString("artist-name", Wt::WString::tr("msg-various-artists"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(artists.front().id())));
|
||||||
|
Wt::WText *artist = new Wt::WText(artistAnchor);
|
||||||
|
artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText));
|
||||||
|
entry->bindWidget("artist-name", artistAnchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WContainerWidget>
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
class Releases : public Wt::WContainerWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Releases(Wt::WContainerWidget* parent = 0);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void refresh(std::vector<std::string> searchKeywords = std::vector<std::string>());
|
||||||
|
|
||||||
|
Wt::WContainerWidget *_releasesContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Wt/WAnchor>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
#include <Wt/WLineEdit>
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#include <Wt/WText>
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
#include "TracksView.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Tracks::Tracks(Wt::WContainerWidget* parent)
|
||||||
|
: Wt::WContainerWidget(parent)
|
||||||
|
{
|
||||||
|
auto tracks = new Wt::WTemplate(Wt::WString::tr("template-tracks"), this);
|
||||||
|
tracks->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
auto search = new Wt::WLineEdit();
|
||||||
|
tracks->bindWidget("search", search);
|
||||||
|
search->setPlaceholderText(Wt::WString::tr("msg-search-placeholder"));
|
||||||
|
search->textInput().connect(std::bind([this, search]
|
||||||
|
{
|
||||||
|
auto keywords = splitString(search->text().toUTF8(), " ");
|
||||||
|
refresh(keywords);
|
||||||
|
}));
|
||||||
|
|
||||||
|
_tracksContainer = new Wt::WContainerWidget();
|
||||||
|
tracks->bindWidget("tracks", _tracksContainer);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Tracks::refresh(std::vector<std::string> searchKeywords)
|
||||||
|
{
|
||||||
|
_tracksContainer->clear();
|
||||||
|
|
||||||
|
std::vector<Cluster::id_type> clusterIds; // TODO fill
|
||||||
|
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
bool moreResults;
|
||||||
|
auto tracks = Track::getByFilter(DboSession(), clusterIds, searchKeywords, 0, 40, moreResults);
|
||||||
|
|
||||||
|
for (auto track : tracks)
|
||||||
|
{
|
||||||
|
Wt::WTemplate* entry = new Wt::WTemplate(Wt::WString::tr("template-tracks-entry"), _tracksContainer);
|
||||||
|
|
||||||
|
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||||
|
|
||||||
|
auto artist = track->getArtist();
|
||||||
|
if (!artist->isNone())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-artist", true);
|
||||||
|
Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(track->getArtist().id())));
|
||||||
|
Wt::WText *artistText = new Wt::WText(artistAnchor);
|
||||||
|
artistText->setText(Wt::WString::fromUTF8(artist->getName(), Wt::PlainText));
|
||||||
|
entry->bindWidget("artist-name", artistAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto release = track->getRelease();
|
||||||
|
if (!release->isNone())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-release", true);
|
||||||
|
// TODO anchor
|
||||||
|
Wt::WAnchor *releaseAnchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(track->getRelease().id())));
|
||||||
|
Wt::WText *releaseText = new Wt::WText(releaseAnchor);
|
||||||
|
releaseText->setText(Wt::WString::fromUTF8(release->getName(), Wt::PlainText));
|
||||||
|
entry->bindWidget("release-name", releaseAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WImage *cover = new Wt::WImage();
|
||||||
|
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
|
||||||
|
// Some images may not be square
|
||||||
|
cover->setWidth(64);
|
||||||
|
entry->bindWidget("cover", cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
* Copyright (C) 2018 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* 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
|
||||||
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WAudio>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
#include "common/InputRange.hpp"
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "AudioPlayer.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
|
|
||||||
Av::Encoding
|
|
||||||
AudioPlayer::getBestEncoding() const
|
|
||||||
{
|
|
||||||
// TODO get the supported formats of the player and pick one
|
|
||||||
return Av::Encoding::MP3;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
AudioPlayer::loadTrack(Database::Track::id_type trackId)
|
|
||||||
{
|
|
||||||
// TODO reduce this scope
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
|
||||||
if (!track)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "No track found for id " << trackId;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::User::pointer user = CurrentUser();
|
|
||||||
if (!user)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "No user found!";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Av::Encoding encoding;
|
|
||||||
switch (user->getAudioEncoding())
|
|
||||||
{
|
|
||||||
case Database::AudioEncoding::MP3: encoding = Av::Encoding::MP3; break;
|
|
||||||
case Database::AudioEncoding::OGA: encoding = Av::Encoding::OGA; break;
|
|
||||||
case Database::AudioEncoding::WEBMA: encoding = Av::Encoding::WEBMA; break;
|
|
||||||
case Database::AudioEncoding::AUTO:
|
|
||||||
default:
|
|
||||||
encoding = getBestEncoding();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
bindString("track", Wt::WString::fromUTF8(track->getName()));
|
|
||||||
bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName()));
|
|
||||||
_cover->setImageLink(SessionImageResource()->getTrackUrl(trackId, 64));
|
|
||||||
|
|
||||||
std::string durationFormat = track->getDuration().total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
|
||||||
|
|
||||||
_trackDuration->setText(durationToString(track->getDuration(), durationFormat));
|
|
||||||
_trackCurTime->setText(durationToString(boost::posix_time::seconds(0), durationFormat));
|
|
||||||
|
|
||||||
// Analyse track, select the best media stream
|
|
||||||
Av::MediaFile mediaFile(track->getPath());
|
|
||||||
|
|
||||||
if (!mediaFile.open() || !mediaFile.scan())
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
|
|
||||||
std::vector<std::size_t> streams;
|
|
||||||
if (audioBestStreamId != -1)
|
|
||||||
streams.push_back(audioBestStreamId);
|
|
||||||
|
|
||||||
this->doJavaScript("\
|
|
||||||
document.lms.audio.state = \"loaded\";\
|
|
||||||
document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
|
|
||||||
document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
|
|
||||||
document.lms.audio.seekbar.value = 0;\
|
|
||||||
document.lms.audio.seekbar.disabled = false;\
|
|
||||||
document.lms.audio.offset = 0;\
|
|
||||||
document.lms.audio.curTime = 0;\
|
|
||||||
");
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Loading, URL = '" << SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams) << "'";
|
|
||||||
|
|
||||||
//TODO, try to load everything in JS in order to prevent the WriteError bug?
|
|
||||||
_audio->pause();
|
|
||||||
_audio->clearSources();
|
|
||||||
_audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams));
|
|
||||||
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
|
||||||
_audio->play();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioPlayer::AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplate(parent)
|
|
||||||
{
|
|
||||||
setTemplateText(Wt::WString::tr("wa-audio-player"));
|
|
||||||
addStyleClass("mediaplayer");
|
|
||||||
|
|
||||||
// TODO potential leak here
|
|
||||||
_audio = new Wt::WAudio();
|
|
||||||
_audio->setOptions(Wt::WAudio::Autoplay);
|
|
||||||
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
|
||||||
bindWidget("audio", _audio);
|
|
||||||
|
|
||||||
_audio->ended().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_playbackEnded.emit();
|
|
||||||
}));
|
|
||||||
|
|
||||||
_cover = new Wt::WImage();
|
|
||||||
bindWidget("cover", _cover);
|
|
||||||
_cover->setImageLink(SessionImageResource()->getUnknownTrackUrl(64));
|
|
||||||
|
|
||||||
InputRange *seekbar = new InputRange();
|
|
||||||
seekbar->addStyleClass("mediaplayer-seekbar");
|
|
||||||
bindWidget("seekbar", seekbar);
|
|
||||||
|
|
||||||
bindString("track", "Track");
|
|
||||||
bindString("artist", "Artist");
|
|
||||||
|
|
||||||
InputRange *volumeSlider = new InputRange();
|
|
||||||
volumeSlider->addStyleClass("mediaplayer-volume");
|
|
||||||
volumeSlider->setAttributeValue("orient", "vertical"); // firefox
|
|
||||||
bindWidget("volume", volumeSlider);
|
|
||||||
|
|
||||||
if (controls & ControlRepeat)
|
|
||||||
{
|
|
||||||
setCondition("if-has-repeat", true);
|
|
||||||
Wt::WText *repeatBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
repeatBtn->addStyleClass("mediaplayer-btn hidden-xs");
|
|
||||||
bindWidget("repeat", repeatBtn);
|
|
||||||
repeatBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
if (repeatBtn->hasStyleClass("mediaplayer-btn-active"))
|
|
||||||
{
|
|
||||||
repeatBtn->removeStyleClass("mediaplayer-btn-active");
|
|
||||||
_loop.emit(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
repeatBtn->addStyleClass("mediaplayer-btn-active");
|
|
||||||
_loop.emit(true);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controls & ControlPlayqueue)
|
|
||||||
{
|
|
||||||
setCondition("if-has-playqueue", true);
|
|
||||||
Wt::WText *playQueueBtn = new Wt::WText("<i class=\"fa fa-list-ul fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
bindWidget("playqueue", playQueueBtn);
|
|
||||||
playQueueBtn->addStyleClass("mediaplayer-btn");
|
|
||||||
playQueueBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_playQueue.emit();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controls & ControlShuffle)
|
|
||||||
{
|
|
||||||
setCondition("if-has-shuffle", true);
|
|
||||||
Wt::WText *shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
shuffleBtn->addStyleClass("mediaplayer-btn hidden-xs");
|
|
||||||
bindWidget("shuffle", shuffleBtn);
|
|
||||||
shuffleBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
if (shuffleBtn->hasStyleClass("mediaplayer-btn-active"))
|
|
||||||
{
|
|
||||||
shuffleBtn->removeStyleClass("mediaplayer-btn-active");
|
|
||||||
_shuffle.emit(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
shuffleBtn->addStyleClass("mediaplayer-btn-active");
|
|
||||||
_shuffle.emit(true);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WText *prevBtn = new Wt::WText("<i class=\"fa fa-step-backward fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
prevBtn->addStyleClass("mediaplayer-btn");
|
|
||||||
bindWidget("prev", prevBtn);
|
|
||||||
prevBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_playPrevious.emit();
|
|
||||||
}));
|
|
||||||
|
|
||||||
Wt::WText *nextBtn = new Wt::WText("<i class=\"fa fa-step-forward fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
nextBtn->addStyleClass("mediaplayer-btn");
|
|
||||||
bindWidget("next", nextBtn);
|
|
||||||
nextBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_playNext.emit();
|
|
||||||
}));
|
|
||||||
|
|
||||||
Wt::WText *playPauseBtn = new Wt::WText("<i class=\"fa fa-play fa-3x fa-fw\"></i>", Wt::XHTMLText);
|
|
||||||
playPauseBtn->addStyleClass("mediaplayer-btn");
|
|
||||||
bindWidget("play-pause", playPauseBtn);
|
|
||||||
|
|
||||||
_trackCurTime = new Wt::WText("--:--");
|
|
||||||
_trackCurTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration");
|
|
||||||
bindWidget("curtime", _trackCurTime);
|
|
||||||
|
|
||||||
_trackDuration = new Wt::WText("--:--");
|
|
||||||
_trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration");
|
|
||||||
bindWidget("duration", _trackDuration);
|
|
||||||
|
|
||||||
this->doJavaScript(
|
|
||||||
"\
|
|
||||||
document.lms = {};\
|
|
||||||
document.lms.audio = {};\
|
|
||||||
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
|
||||||
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
|
||||||
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
|
||||||
document.lms.audio.curTimeText = " + _trackCurTime->jsRef() + ";\
|
|
||||||
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
|
||||||
\
|
|
||||||
document.lms.audio.offset = 0;\
|
|
||||||
document.lms.audio.curTime = 0;\
|
|
||||||
document.lms.audio.state = \"init\";\
|
|
||||||
document.lms.audio.volume = 1;\
|
|
||||||
\
|
|
||||||
document.lms.audio.seekbar.value = 0;\
|
|
||||||
document.lms.audio.seekbar.disabled = true;\
|
|
||||||
\
|
|
||||||
document.lms.audio.volumeSlider.min = 0;\
|
|
||||||
document.lms.audio.volumeSlider.max = 100;\
|
|
||||||
document.lms.audio.volumeSlider.value = 100;\
|
|
||||||
\
|
|
||||||
function updateUIPlaying() { \
|
|
||||||
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
|
||||||
icon.className = \"fa fa-pause fa-3x fa-fw\"; \
|
|
||||||
} \
|
|
||||||
\
|
|
||||||
function updateUIStopped() { \
|
|
||||||
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
|
||||||
icon.className = \"fa fa-play fa-3x fa-fw\"; \
|
|
||||||
} \
|
|
||||||
\
|
|
||||||
function durationToString(duration, displayHours) { \
|
|
||||||
var hours = parseInt( duration / 3600 ) % 24; \
|
|
||||||
var minutes = parseInt( duration / 60) % 60; \
|
|
||||||
var seconds = duration % 60; \
|
|
||||||
\
|
|
||||||
var res = \"\"; \
|
|
||||||
if (displayHours) \
|
|
||||||
res = (hours < 10 ? \"0\" + hours : hours) + \":\"; \
|
|
||||||
\
|
|
||||||
res += (minutes < 10 ? \"0\" + minutes : minutes) + \":\"; \
|
|
||||||
res += (seconds < 10 ? \"0\" + seconds : seconds); \
|
|
||||||
return res; \
|
|
||||||
} \
|
|
||||||
\
|
|
||||||
function updateUI() {\
|
|
||||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
|
||||||
document.lms.audio.seekbar.value = document.lms.audio.curTime;\
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
var mouseDown = 0;\
|
|
||||||
function seekMouseDown(e) {\
|
|
||||||
++mouseDown;\
|
|
||||||
}\
|
|
||||||
function seekMouseUp(e) {\
|
|
||||||
--mouseDown;\
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
function seeking(e) {\
|
|
||||||
if (document.lms.audio.state == \"init\")\
|
|
||||||
return;\
|
|
||||||
\
|
|
||||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.seekbar.value, document.lms.audio.seekbar.max > 3600); \
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
function seek(e) {\
|
|
||||||
if (document.lms.audio.state == \"init\")\
|
|
||||||
return;\
|
|
||||||
\
|
|
||||||
document.lms.audio.audio.pause(); \
|
|
||||||
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
|
||||||
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
|
||||||
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
|
||||||
var src = audioSource.src;\
|
|
||||||
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
|
||||||
audioSource.src = src + document.lms.audio.seekbar.value;\
|
|
||||||
document.lms.audio.audio.load(); \
|
|
||||||
document.lms.audio.audio.play(); \
|
|
||||||
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
function volumeChanged() {\
|
|
||||||
document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
function updateCurTime() {\
|
|
||||||
document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
|
|
||||||
if (mouseDown == 0)\
|
|
||||||
updateUI();\
|
|
||||||
} \
|
|
||||||
\
|
|
||||||
function playPause() { \
|
|
||||||
if (document.lms.audio.state == \"init\") \
|
|
||||||
return;\
|
|
||||||
\
|
|
||||||
if (document.lms.audio.audio.paused) \
|
|
||||||
document.lms.audio.audio.play(); \
|
|
||||||
else \
|
|
||||||
document.lms.audio.audio.pause();\
|
|
||||||
\
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
|
|
||||||
document.lms.audio.audio.addEventListener('playing', updateUIPlaying); \
|
|
||||||
document.lms.audio.audio.addEventListener('play', updateUIPlaying); \
|
|
||||||
document.lms.audio.audio.addEventListener('pause', updateUIStopped); \
|
|
||||||
document.lms.audio.audio.addEventListener('ended', updateUIStopped); \
|
|
||||||
document.lms.audio.seekbar.addEventListener('change', seek);\
|
|
||||||
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
|
||||||
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
|
||||||
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
|
||||||
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
|
||||||
document.lms.audio.playPause.addEventListener('click', playPause);\
|
|
||||||
"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // namespaceUserInterface
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "common/InputRange.hpp"
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
#include "av/AvTranscoder.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class AudioPlayer : public Wt::WTemplate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
enum ControlFlags
|
|
||||||
{
|
|
||||||
ControlNone = 0,
|
|
||||||
ControlShuffle = 1 << 0,
|
|
||||||
ControlRepeat = 1 << 1,
|
|
||||||
ControlPlayqueue = 1 << 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
AudioPlayer(ControlFlags controls, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
Av::Encoding getBestEncoding() const;
|
|
||||||
bool loadTrack(Database::Track::id_type trackId);
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
Wt::Signal<void>& playbackEnded() {return _playbackEnded;}
|
|
||||||
Wt::Signal<void>& playNext() {return _playNext;}
|
|
||||||
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
|
||||||
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
|
||||||
Wt::Signal<bool>& loop() {return _loop;}
|
|
||||||
Wt::Signal<void>& showPlayQueue() {return _playQueue;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
Wt::Signal<void> _playbackEnded;
|
|
||||||
Wt::Signal<void> _playNext;
|
|
||||||
Wt::Signal<void> _playPrevious;
|
|
||||||
Wt::Signal<bool> _shuffle;
|
|
||||||
Wt::Signal<bool> _loop;
|
|
||||||
Wt::Signal<void> _playQueue;
|
|
||||||
|
|
||||||
Wt::WAudio* _audio;
|
|
||||||
Wt::WText* _trackCurTime;
|
|
||||||
Wt::WText* _trackDuration;
|
|
||||||
Wt::WText* _trackName;
|
|
||||||
Wt::WImage* _cover;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr AudioPlayer::ControlFlags operator|(AudioPlayer::ControlFlags f1, AudioPlayer::ControlFlags f2) { return AudioPlayer::ControlFlags(int(f1)|int(f2)); }
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,556 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/bind.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WVBoxLayout>
|
|
||||||
#include <Wt/WHBoxLayout>
|
|
||||||
#include <Wt/WGridLayout>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WMessageBox>
|
|
||||||
|
|
||||||
#include <Wt/WLengthValidator>
|
|
||||||
#include <Wt/WDialog>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WLabel>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "TableFilter.hpp"
|
|
||||||
#include "KeywordSearchFilter.hpp"
|
|
||||||
|
|
||||||
#include "DesktopAudio.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void WPopupMenuClear(Wt::WPopupMenu* menu)
|
|
||||||
{
|
|
||||||
while(menu->count() > 0)
|
|
||||||
menu->removeItem(menu->itemAt(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
// Special playlist generated each time the playque gets changed
|
|
||||||
// Restored at the beginning of the session
|
|
||||||
static const std::string CurrentQueuePlaylistName = "__current__";
|
|
||||||
|
|
||||||
Audio::Audio(Wt::WContainerWidget* parent)
|
|
||||||
: UserInterface::Audio(parent),
|
|
||||||
_mediaPlayer(nullptr),
|
|
||||||
_trackView(nullptr),
|
|
||||||
_playQueue(nullptr)
|
|
||||||
{
|
|
||||||
Wt::WGridLayout *mainLayout = new Wt::WGridLayout();
|
|
||||||
this->setLayout(mainLayout);
|
|
||||||
mainLayout->setContentsMargins(9,4,9,9);
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
Wt::WHBoxLayout *filterLayout = new Wt::WHBoxLayout();
|
|
||||||
|
|
||||||
TableFilterCluster *filterCluster = new TableFilterCluster();
|
|
||||||
filterLayout->addWidget(filterCluster);
|
|
||||||
_filterChain.addFilter(filterCluster);
|
|
||||||
|
|
||||||
TableFilterArtist *filterArtist = new TableFilterArtist();
|
|
||||||
filterLayout->addWidget(filterArtist);
|
|
||||||
_filterChain.addFilter(filterArtist);
|
|
||||||
|
|
||||||
TableFilterRelease *filterRelease = new TableFilterRelease();
|
|
||||||
filterLayout->addWidget(filterRelease);
|
|
||||||
_filterChain.addFilter(filterRelease);
|
|
||||||
|
|
||||||
mainLayout->addLayout(filterLayout, 0, 1);
|
|
||||||
// ENDOF(Filters)
|
|
||||||
|
|
||||||
// TODO ADD some stats (nb files, total duration, etc.)
|
|
||||||
|
|
||||||
Wt::WVBoxLayout* trackLayout = new Wt::WVBoxLayout();
|
|
||||||
|
|
||||||
_trackView = new TrackView();
|
|
||||||
trackLayout->addWidget(_trackView, 1);
|
|
||||||
|
|
||||||
Wt::WHBoxLayout* trackControls = new Wt::WHBoxLayout();
|
|
||||||
|
|
||||||
Wt::WPushButton* playBtn = new Wt::WPushButton("Play");
|
|
||||||
playBtn->setStyleClass("btn-sm");
|
|
||||||
trackControls->addWidget(playBtn);
|
|
||||||
|
|
||||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add");
|
|
||||||
addBtn->setStyleClass("btn-sm");
|
|
||||||
trackControls->addWidget(addBtn);
|
|
||||||
|
|
||||||
Wt::WText *statsText = new Wt::WText();
|
|
||||||
statsText->setStyleClass("vertical-align");
|
|
||||||
trackControls->addWidget(statsText, 1);
|
|
||||||
_trackView->statsUpdated().connect(std::bind([=] (Wt::WString stats) {
|
|
||||||
statsText->setText(stats);
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
|
|
||||||
trackLayout->addLayout(trackControls);
|
|
||||||
|
|
||||||
mainLayout->addLayout(trackLayout, 1, 1);
|
|
||||||
|
|
||||||
_filterChain.addFilter(_trackView);
|
|
||||||
|
|
||||||
|
|
||||||
_playQueue = new PlayQueue();
|
|
||||||
|
|
||||||
// Playlist/PlayQueue
|
|
||||||
Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
|
|
||||||
playQueueContainer->setStyleClass("playqueue");
|
|
||||||
Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
|
|
||||||
playQueueContainer->setLayout(playQueueLayout);
|
|
||||||
|
|
||||||
playQueueLayout->addWidget( _playQueue, 1);
|
|
||||||
|
|
||||||
Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
|
|
||||||
|
|
||||||
Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
|
|
||||||
playlistBtn->setStyleClass("btn-sm btn-primary");
|
|
||||||
playlistControls->addWidget(playlistBtn);
|
|
||||||
|
|
||||||
// Playlist menu
|
|
||||||
{
|
|
||||||
Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
|
|
||||||
|
|
||||||
_popupMenuSave = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Save", _popupMenuSave);
|
|
||||||
|
|
||||||
_popupMenuLoad = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Load", _popupMenuLoad);
|
|
||||||
|
|
||||||
_popupMenuDelete = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Delete", _popupMenuDelete);
|
|
||||||
|
|
||||||
playlistBtn->setMenu(popupMain);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
Wt::WPushButton *upBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-up fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
#else
|
|
||||||
Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
|
|
||||||
#endif
|
|
||||||
upBtn->setStyleClass("btn-sm");
|
|
||||||
playlistControls->addWidget(upBtn);
|
|
||||||
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
Wt::WPushButton *downBtn = new Wt::WPushButton("<i class =\"fa fa-arrow-down fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
#else
|
|
||||||
Wt::WPushButton *downBtn = new Wt::WPushButton("DOWN");
|
|
||||||
#endif
|
|
||||||
downBtn->setStyleClass("btn-sm");
|
|
||||||
playlistControls->addWidget(downBtn);
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
Wt::WPushButton *delBtn = new Wt::WPushButton("<i class =\"fa fa-remove fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
#else
|
|
||||||
Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
delBtn->setStyleClass("btn-sm btn-warning");
|
|
||||||
playlistControls->addWidget(delBtn);
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
Wt::WPushButton *clearBtn = new Wt::WPushButton("<i class =\"fa fa-trash fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
#else
|
|
||||||
Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
|
|
||||||
#endif
|
|
||||||
clearBtn->setStyleClass("btn-sm btn-danger");
|
|
||||||
playlistControls->addWidget(clearBtn);
|
|
||||||
|
|
||||||
delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
|
|
||||||
upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
|
|
||||||
downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
|
|
||||||
clearBtn->clicked().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
|
||||||
("Clear playqueue",
|
|
||||||
Wt::WString( "Are you sure?"),
|
|
||||||
Wt::Question, Wt::Yes | Wt::No);
|
|
||||||
|
|
||||||
messageBox->setModal(true);
|
|
||||||
|
|
||||||
messageBox->buttonClicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
if (messageBox->buttonResult() == Wt::Yes)
|
|
||||||
_playQueue->delAll();
|
|
||||||
|
|
||||||
delete messageBox;
|
|
||||||
}));
|
|
||||||
|
|
||||||
messageBox->show();
|
|
||||||
}));
|
|
||||||
|
|
||||||
playQueueLayout->addLayout(playlistControls);
|
|
||||||
|
|
||||||
mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
|
|
||||||
|
|
||||||
// Load the last known queue
|
|
||||||
playlistLoadToPlayqueue(CurrentQueuePlaylistName);
|
|
||||||
|
|
||||||
// Select the last known playing track
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
|
|
||||||
}
|
|
||||||
|
|
||||||
_mediaPlayer = new AudioPlayer(AudioPlayer::ControlShuffle | AudioPlayer::ControlRepeat);
|
|
||||||
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
|
|
||||||
|
|
||||||
mainLayout->setRowStretch(1, 1);
|
|
||||||
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
|
|
||||||
mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel));
|
|
||||||
|
|
||||||
// Double click on track
|
|
||||||
// Set the selected tracks to the play queue
|
|
||||||
_trackView->trackDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddSelectedTracks));
|
|
||||||
|
|
||||||
// Double click on artist
|
|
||||||
// Set the selected tracks to the play queue
|
|
||||||
filterArtist->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks));
|
|
||||||
filterRelease->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks));
|
|
||||||
filterCluster->sigDoubleClicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddAllTracks));
|
|
||||||
|
|
||||||
// Play button
|
|
||||||
// Set the selected tracks to the play queue
|
|
||||||
playBtn->clicked().connect(boost::bind(&Audio::playSelectedTracks, this, PlayQueueAddSelectedTracks));
|
|
||||||
|
|
||||||
// Add Button
|
|
||||||
// Add the selected tracks at the end of the play queue
|
|
||||||
addBtn->clicked().connect(this, &Audio::addSelectedTracks);
|
|
||||||
|
|
||||||
_playQueue->playTrack().connect(this, &Audio::playTrack);
|
|
||||||
|
|
||||||
_mediaPlayer->playbackEnded().connect(_playQueue, &PlayQueue::handlePlaybackComplete);
|
|
||||||
_mediaPlayer->playNext().connect(_playQueue, &PlayQueue::playNext);
|
|
||||||
_mediaPlayer->playPrevious().connect(_playQueue, &PlayQueue::playPrevious);
|
|
||||||
_mediaPlayer->shuffle().connect(boost::bind(&PlayQueue::setShuffle, _playQueue, _1));
|
|
||||||
_mediaPlayer->loop().connect(boost::bind(&PlayQueue::setLoop,_playQueue, _1));
|
|
||||||
|
|
||||||
_playQueue->tracksUpdated().connect(std::bind([=] () {
|
|
||||||
LMS_LOG(UI, INFO) << "Playqueue updated!";
|
|
||||||
|
|
||||||
playlistSaveFromPlayqueue(CurrentQueuePlaylistName);
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
playlistRefreshMenus();
|
|
||||||
|
|
||||||
// Initially, search for everything
|
|
||||||
_filterChain.searchKeyword("");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistShowSaveNewDialog()
|
|
||||||
{
|
|
||||||
Wt::WDialog *dialog = new Wt::WDialog("New playlist");
|
|
||||||
|
|
||||||
Wt::WLabel *label = new Wt::WLabel("Name", dialog->contents());
|
|
||||||
Wt::WLineEdit *edit = new Wt::WLineEdit(dialog->contents());
|
|
||||||
label->setBuddy(edit);
|
|
||||||
|
|
||||||
Wt::WLengthValidator* validator = new Wt::WLengthValidator();
|
|
||||||
validator->setMinimumLength(3);
|
|
||||||
validator->setMandatory(true);
|
|
||||||
edit->setValidator(validator);
|
|
||||||
|
|
||||||
Wt::WPushButton *save = new Wt::WPushButton("Save", dialog->footer());
|
|
||||||
save->setStyleClass("btn-success");
|
|
||||||
save->setDefault(true);
|
|
||||||
save->disable();
|
|
||||||
|
|
||||||
Wt::WPushButton *cancel = new Wt::WPushButton("Cancel", dialog->footer());
|
|
||||||
dialog->rejectWhenEscapePressed();
|
|
||||||
|
|
||||||
edit->keyWentUp().connect(std::bind([=] () {
|
|
||||||
save->setDisabled(edit->validate() != Wt::WValidator::Valid);
|
|
||||||
}));
|
|
||||||
|
|
||||||
save->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
if (edit->validate())
|
|
||||||
dialog->accept();
|
|
||||||
}));
|
|
||||||
|
|
||||||
cancel->clicked().connect(dialog, &Wt::WDialog::reject);
|
|
||||||
|
|
||||||
dialog->finished().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
if (dialog->result() == Wt::WDialog::Accepted)
|
|
||||||
{
|
|
||||||
playlistShowSaveDialog(edit->text().toUTF8());
|
|
||||||
}
|
|
||||||
|
|
||||||
delete dialog;
|
|
||||||
}));
|
|
||||||
|
|
||||||
dialog->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistShowSaveDialog(std::string playlistName)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Actually create the dialog only if the given list already exists
|
|
||||||
if (Playlist::get(DboSession(), playlistName, CurrentUser()))
|
|
||||||
{
|
|
||||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
|
||||||
("Overwrite playlist",
|
|
||||||
Wt::WString( "Overwrite playlist '{1}'?").arg(playlistName),
|
|
||||||
Wt::Question, Wt::Yes | Wt::No);
|
|
||||||
|
|
||||||
messageBox->setModal(true);
|
|
||||||
|
|
||||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
|
||||||
if (messageBox->buttonResult() == Wt::Yes)
|
|
||||||
playlistSaveFromPlayqueue(playlistName);
|
|
||||||
|
|
||||||
delete messageBox;
|
|
||||||
}));
|
|
||||||
|
|
||||||
messageBox->show();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
playlistSaveFromPlayqueue(playlistName);
|
|
||||||
playlistRefreshMenus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistSaveFromPlayqueue(std::string playlistName)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Saving playqueue to playlist '" << playlistName << "'";
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Playlist::pointer playlist = Playlist::get(DboSession(), playlistName, CurrentUser());
|
|
||||||
if (playlist)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Erasing playlist '" << playlistName << "'";
|
|
||||||
playlist.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist = Playlist::create(DboSession(), playlistName, false, CurrentUser());
|
|
||||||
|
|
||||||
std::vector<Track::id_type> trackIds;
|
|
||||||
_playQueue->getTracks(trackIds);
|
|
||||||
|
|
||||||
int pos = 0;
|
|
||||||
for (Track::id_type trackId : trackIds)
|
|
||||||
{
|
|
||||||
Track::pointer track = Track::getById(DboSession(), trackId);
|
|
||||||
|
|
||||||
if (track)
|
|
||||||
PlaylistEntry::create(DboSession(), track, playlist, pos++);
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "Saving playqueue to playlist '" << playlistName << "' done. Contains " << pos << " entries";
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistLoadToPlayqueue(std::string playlistName)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Loading playlist '" << playlistName << "' to playqueue";
|
|
||||||
|
|
||||||
std::vector<Track::id_type> entries;
|
|
||||||
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Playlist::pointer playlist = Playlist::get(DboSession(), playlistName, CurrentUser());
|
|
||||||
if (!playlist)
|
|
||||||
return;
|
|
||||||
|
|
||||||
entries = PlaylistEntry::getEntries(DboSession(), playlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
_playQueue->clear();
|
|
||||||
_playQueue->addTracks(entries);
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Loading playlist '" << playlistName << "' to playqueue done. " << entries.size() << " entries";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistShowDeleteDialog(std::string name)
|
|
||||||
{
|
|
||||||
|
|
||||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
|
||||||
("Delete playlist",
|
|
||||||
Wt::WString( "Deleting playlist '{1}'?").arg(name),
|
|
||||||
Wt::Question, Wt::Yes | Wt::No);
|
|
||||||
|
|
||||||
messageBox->setModal(true);
|
|
||||||
|
|
||||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
|
||||||
if (messageBox->buttonResult() == Wt::Yes)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Playlist::pointer playlist = Playlist::get(DboSession(), name, CurrentUser());
|
|
||||||
if (playlist)
|
|
||||||
playlist.remove();
|
|
||||||
|
|
||||||
playlistRefreshMenus();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete messageBox;
|
|
||||||
}));
|
|
||||||
|
|
||||||
messageBox->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playlistRefreshMenus()
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Clear playlists in each menu
|
|
||||||
LMS_LOG(UI, DEBUG) << "Save item count: " << _popupMenuSave->count();
|
|
||||||
|
|
||||||
WPopupMenuClear(_popupMenuDelete);
|
|
||||||
WPopupMenuClear(_popupMenuLoad);
|
|
||||||
WPopupMenuClear(_popupMenuSave);
|
|
||||||
|
|
||||||
_popupMenuSave->addItem("New")->triggered().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
playlistShowSaveNewDialog();
|
|
||||||
}));
|
|
||||||
_popupMenuSave->addSeparator();
|
|
||||||
|
|
||||||
std::vector<Playlist::pointer> playlists = Playlist::get(DboSession(), CurrentUser());
|
|
||||||
|
|
||||||
for (Playlist::pointer playlist : playlists)
|
|
||||||
{
|
|
||||||
if (playlist->getName() == CurrentQueuePlaylistName)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Add playlists in each menu
|
|
||||||
_popupMenuDelete->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
playlistShowDeleteDialog(playlist->getName());
|
|
||||||
}));
|
|
||||||
|
|
||||||
_popupMenuLoad->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
playlistLoadToPlayqueue(playlist->getName());
|
|
||||||
playlistRefreshMenus(); // in case deleted in other session
|
|
||||||
}));
|
|
||||||
|
|
||||||
_popupMenuSave->addItem(playlist->getName())->triggered().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
playlistShowSaveDialog(playlist->getName());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::search(std::string searchText)
|
|
||||||
{
|
|
||||||
_filterChain.searchKeyword(searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::addSelectedTracks(void)
|
|
||||||
{
|
|
||||||
std::vector<Track::id_type> trackIds;
|
|
||||||
_trackView->getSelectedTracks(trackIds);
|
|
||||||
|
|
||||||
// If nothing is selected, get the whole track list
|
|
||||||
if (trackIds.empty())
|
|
||||||
_trackView->getTracks(trackIds);
|
|
||||||
|
|
||||||
_playQueue->addTracks(trackIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playSelectedTracks(PlayQueueAddType addType)
|
|
||||||
{
|
|
||||||
std::vector<Track::id_type> trackIds;
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Playing selected tracks... nb selected = " << _trackView->getNbSelectedTracks() << ", add type = " << (addType == PlayQueueAddAllTracks ? "AddAll" : "AddSelected");
|
|
||||||
|
|
||||||
_playQueue->clear();
|
|
||||||
|
|
||||||
switch(addType)
|
|
||||||
{
|
|
||||||
case PlayQueueAddAllTracks:
|
|
||||||
// Play all the tracks
|
|
||||||
_trackView->getTracks(trackIds);
|
|
||||||
_playQueue->addTracks(trackIds);
|
|
||||||
_playQueue->play();
|
|
||||||
|
|
||||||
break;
|
|
||||||
case PlayQueueAddSelectedTracks:
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Adding selected tracks...";
|
|
||||||
|
|
||||||
// If nothing selected, get all the track and play everything
|
|
||||||
if (_trackView->getNbSelectedTracks() == 0)
|
|
||||||
{
|
|
||||||
_trackView->getTracks(trackIds);
|
|
||||||
_playQueue->addTracks(trackIds);
|
|
||||||
_playQueue->play();
|
|
||||||
}
|
|
||||||
// If only ONE selected, get them all and pre select the track
|
|
||||||
else if (_trackView->getNbSelectedTracks() == 1)
|
|
||||||
{
|
|
||||||
// Start to play at the selected track, if any
|
|
||||||
_trackView->getTracks(trackIds);
|
|
||||||
_playQueue->addTracks(trackIds);
|
|
||||||
_playQueue->play(_trackView->getFirstSelectedTrackPosition());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Play all the selected tracks
|
|
||||||
_trackView->getSelectedTracks(trackIds);
|
|
||||||
_playQueue->addTracks(trackIds);
|
|
||||||
_playQueue->play();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::playTrack(Track::id_type trackId, int pos)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Update user current track position
|
|
||||||
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_mediaPlayer->loadTrack(trackId))
|
|
||||||
_playQueue->playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <Wt/WPopupMenu>
|
|
||||||
|
|
||||||
#include "audio/AudioPlayer.hpp"
|
|
||||||
#include "TrackView.hpp"
|
|
||||||
#include "PlayQueue.hpp"
|
|
||||||
|
|
||||||
#include "FilterChain.hpp"
|
|
||||||
|
|
||||||
#include "audio/Audio.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class Audio : public UserInterface::Audio
|
|
||||||
{
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
Audio(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
void search(std::string searchText);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void playlistSaveFromPlayqueue(std::string name);
|
|
||||||
void playlistLoadToPlayqueue(std::string name);
|
|
||||||
void playlistShowSaveNewDialog();
|
|
||||||
void playlistShowSaveDialog(std::string name);
|
|
||||||
void playlistShowDeleteDialog(std::string name);
|
|
||||||
void playlistRefreshMenus();
|
|
||||||
|
|
||||||
void playTrack(Database::Track::id_type id, int pos);
|
|
||||||
|
|
||||||
enum PlayQueueAddType
|
|
||||||
{
|
|
||||||
PlayQueueAddAllTracks,
|
|
||||||
PlayQueueAddSelectedTracks,
|
|
||||||
};
|
|
||||||
|
|
||||||
void playSelectedTracks(PlayQueueAddType addType);
|
|
||||||
void addSelectedTracks();
|
|
||||||
|
|
||||||
void handlePlaylistSelected(Wt::WString name);
|
|
||||||
|
|
||||||
AudioPlayer* _mediaPlayer;
|
|
||||||
TrackView* _trackView;
|
|
||||||
PlayQueue* _playQueue;
|
|
||||||
|
|
||||||
FilterChain _filterChain;
|
|
||||||
|
|
||||||
Wt::WPopupMenu* _popupMenuSave;
|
|
||||||
Wt::WPopupMenu* _popupMenuLoad;
|
|
||||||
Wt::WPopupMenu* _popupMenuDelete;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FILTER_HPP
|
|
||||||
#define FILTER_HPP
|
|
||||||
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Filter() {}
|
|
||||||
virtual ~Filter() {}
|
|
||||||
|
|
||||||
// Refresh filter using constraints created by parent filters
|
|
||||||
virtual void refresh(Database::SearchFilter& filter) = 0;
|
|
||||||
|
|
||||||
// Update constraints for next filters
|
|
||||||
virtual void getConstraint(Database::SearchFilter& filter) = 0;
|
|
||||||
|
|
||||||
// Emitted when a constraint has changed
|
|
||||||
Wt::Signal<void>& update() { return _update; };
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
void emitUpdate() { _update.emit(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _update;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Dekstop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FilterChain.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
|
|
||||||
FilterChain::FilterChain()
|
|
||||||
: _refreshingFilters(false)
|
|
||||||
{
|
|
||||||
addFilter(&_keywordSearchFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FilterChain::addFilter(Filter* filter)
|
|
||||||
{
|
|
||||||
filter->update().connect(boost::bind(&FilterChain::updateFilters, this, _filters.size()));
|
|
||||||
_filters.push_back(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FilterChain::searchKeyword(const std::string& text)
|
|
||||||
{
|
|
||||||
_keywordSearchFilter.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FilterChain::updateFilters(std::size_t startIdx)
|
|
||||||
{
|
|
||||||
// Prevent loops
|
|
||||||
if (_refreshingFilters)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_refreshingFilters = true;
|
|
||||||
|
|
||||||
Database::SearchFilter searchFilter;
|
|
||||||
|
|
||||||
for (std::size_t idFilter = 0; idFilter < _filters.size(); ++idFilter)
|
|
||||||
{
|
|
||||||
Filter* filter = _filters.at(idFilter);
|
|
||||||
|
|
||||||
// Apply contraints created by previous filters
|
|
||||||
if (idFilter > startIdx) {
|
|
||||||
filter->refresh(searchFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get constraints generated by this filter
|
|
||||||
// (Note: adding accross successive calls)
|
|
||||||
filter->getConstraint(searchFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshingFilters = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FILTER_CHAIN_HPP
|
|
||||||
#define FILTER_CHAIN_HPP
|
|
||||||
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "Filter.hpp"
|
|
||||||
#include "KeywordSearchFilter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
// FilterChain
|
|
||||||
class FilterChain
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
FilterChain();
|
|
||||||
|
|
||||||
void addFilter(Filter* filter);
|
|
||||||
|
|
||||||
// First filter is a keywork search
|
|
||||||
void searchKeyword(const std::string& text);
|
|
||||||
|
|
||||||
// Update filters from filter @ startIdx
|
|
||||||
void updateFilters(std::size_t startIdx);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
KeywordSearchFilter _keywordSearchFilter;
|
|
||||||
|
|
||||||
// No ownership
|
|
||||||
std::vector<Filter*> _filters;
|
|
||||||
|
|
||||||
bool _refreshingFilters;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
|
||||||
#include <boost/algorithm/string.hpp>
|
|
||||||
#include <boost/foreach.hpp>
|
|
||||||
|
|
||||||
#include "KeywordSearchFilter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
KeywordSearchFilter::KeywordSearchFilter()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
KeywordSearchFilter::setText(const std::string& text)
|
|
||||||
{
|
|
||||||
_lastEmittedText = text;
|
|
||||||
emitUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get constraints created by this filter
|
|
||||||
void
|
|
||||||
KeywordSearchFilter::getConstraint(Database::SearchFilter& filter)
|
|
||||||
{
|
|
||||||
// No active search means no constaint!
|
|
||||||
if (!_lastEmittedText.empty()) {
|
|
||||||
std::vector<std::string> values;
|
|
||||||
boost::algorithm::split(values, _lastEmittedText, boost::is_any_of(" "), boost::token_compress_on);
|
|
||||||
|
|
||||||
// For each part, do a global search on all searchable fields
|
|
||||||
for (const std::string& value : values)
|
|
||||||
{
|
|
||||||
filter.nameLikeMatch.push_back({
|
|
||||||
{Database::SearchFilter::Field::Artist, {value}},
|
|
||||||
{Database::SearchFilter::Field::Release, {value}},
|
|
||||||
{Database::SearchFilter::Field::Cluster, {value}},
|
|
||||||
{Database::SearchFilter::Field::Track, {value}}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef KEYWORD_SEARCH_FILTER_HPP
|
|
||||||
#define KEYWORD_SEARCH_FILTER_HPP
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "Filter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class KeywordSearchFilter : public Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
KeywordSearchFilter();
|
|
||||||
|
|
||||||
void setText(const std::string& text);
|
|
||||||
|
|
||||||
// Set constraint on this filter
|
|
||||||
void refresh(Database::SearchFilter& filter) {}
|
|
||||||
|
|
||||||
// Get constraints created by this filter
|
|
||||||
void getConstraint(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void handleKeyWentUp(void);
|
|
||||||
|
|
||||||
std::string _lastEmittedText;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,634 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/random/mersenne_twister.hpp>
|
|
||||||
#include <boost/random/uniform_int_distribution.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WItemDelegate>
|
|
||||||
#include <Wt/WStandardItem>
|
|
||||||
#include <Wt/WFileResource>
|
|
||||||
#include <Wt/WTheme>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "PlayQueue.hpp"
|
|
||||||
|
|
||||||
static const int TrackInfoRole = Wt::UserRole;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void swapRows(Wt::WStandardItemModel *model, int row1, int row2)
|
|
||||||
{
|
|
||||||
// Swap data column by column
|
|
||||||
for (int i = 0; i < model->columnCount(); ++i)
|
|
||||||
{
|
|
||||||
Wt::WModelIndex index1 = model->index(row1, i);
|
|
||||||
Wt::WModelIndex index2 = model->index(row2, i);
|
|
||||||
|
|
||||||
// swap data associated with standard roles
|
|
||||||
{
|
|
||||||
auto tmp = model->itemData(index1); // 1 -> tmp
|
|
||||||
model->setItemData(index1, model->itemData(index2)); // 2 -> 1
|
|
||||||
model->setItemData(index2, tmp); // tmp-> 2
|
|
||||||
}
|
|
||||||
// caution: swap data associated with our custom roles if any!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
enum ColumnId
|
|
||||||
{
|
|
||||||
COLUMN_ID_TRACK_ID = 0,
|
|
||||||
COLUMN_ID_COVER = 1,
|
|
||||||
COLUMN_ID_NAME = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const int trackPosInvalid = -1;
|
|
||||||
|
|
||||||
class TrackSelector
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TrackSelector();
|
|
||||||
|
|
||||||
void setShuffle(bool enable);
|
|
||||||
void setLoop(bool enable) { _loop = enable; }
|
|
||||||
|
|
||||||
int previous(void);
|
|
||||||
int next(void);
|
|
||||||
int getCurrent(void);
|
|
||||||
|
|
||||||
// Set the internal pos thanks to the track pos
|
|
||||||
void setPosByRowId(int rowId);
|
|
||||||
void setPos(int pos) { _curPos = pos; }
|
|
||||||
|
|
||||||
void setSize(std::size_t size);
|
|
||||||
std::size_t getSize(void) const { return _size;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void refreshPositions();
|
|
||||||
|
|
||||||
bool _loop;
|
|
||||||
bool _shuffle;
|
|
||||||
|
|
||||||
std::size_t _size;
|
|
||||||
std::size_t _curPos;
|
|
||||||
|
|
||||||
std::vector<int> _trackPos;
|
|
||||||
};
|
|
||||||
|
|
||||||
TrackSelector::TrackSelector()
|
|
||||||
: _loop(false),
|
|
||||||
_shuffle(false),
|
|
||||||
_size(0),
|
|
||||||
_curPos(0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSelector::refreshPositions()
|
|
||||||
{
|
|
||||||
_trackPos.clear();
|
|
||||||
|
|
||||||
if (_size == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < _size; ++i)
|
|
||||||
_trackPos.push_back(i);
|
|
||||||
|
|
||||||
// Now shuffle
|
|
||||||
// Source: http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
|
||||||
boost::random::mt19937 rng; // produces randomness out of thin air
|
|
||||||
rng.seed(static_cast<unsigned int>(std::time(0)));
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < _size; ++i)
|
|
||||||
{
|
|
||||||
boost::random::uniform_int_distribution<> distribution(i, _size - 1);
|
|
||||||
int val1 = distribution(rng);
|
|
||||||
int tmp = _trackPos[i];
|
|
||||||
_trackPos[i] = _trackPos[val1];
|
|
||||||
_trackPos[val1] = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
TrackSelector::getCurrent()
|
|
||||||
{
|
|
||||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSelector::setShuffle(bool enable)
|
|
||||||
{
|
|
||||||
_shuffle = enable;
|
|
||||||
|
|
||||||
if (_size != 0)
|
|
||||||
{
|
|
||||||
// Update the current index we are playing if we switch shuffle
|
|
||||||
if (_shuffle)
|
|
||||||
setPosByRowId(_curPos);
|
|
||||||
else
|
|
||||||
setPos(_trackPos[_curPos]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
TrackSelector::next()
|
|
||||||
{
|
|
||||||
if (_size == 0)
|
|
||||||
return trackPosInvalid;
|
|
||||||
|
|
||||||
if (_curPos == _size - 1)
|
|
||||||
{
|
|
||||||
if (!_loop)
|
|
||||||
return trackPosInvalid;
|
|
||||||
else
|
|
||||||
_curPos = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
++_curPos;
|
|
||||||
|
|
||||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
TrackSelector::previous()
|
|
||||||
{
|
|
||||||
if (_size == 0)
|
|
||||||
return trackPosInvalid;
|
|
||||||
|
|
||||||
if (_curPos == 0)
|
|
||||||
{
|
|
||||||
if (!_loop)
|
|
||||||
return trackPosInvalid;
|
|
||||||
else
|
|
||||||
_curPos = _size - 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
--_curPos;
|
|
||||||
|
|
||||||
return _shuffle ? _trackPos[_curPos] : _curPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSelector::setPosByRowId(int rowId)
|
|
||||||
{
|
|
||||||
if (_shuffle)
|
|
||||||
{
|
|
||||||
for (std::size_t i = 0; i < _trackPos.size(); ++i)
|
|
||||||
{
|
|
||||||
if (rowId == _trackPos[i])
|
|
||||||
{
|
|
||||||
_curPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
_curPos = rowId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSelector::setSize(std::size_t size)
|
|
||||||
{
|
|
||||||
_size = size;
|
|
||||||
_curPos = 0;
|
|
||||||
refreshPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackInfo
|
|
||||||
{
|
|
||||||
Wt::WString track;
|
|
||||||
Wt::WString artist;
|
|
||||||
Wt::WString release;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PlayQueueItemDelegate : public Wt::WItemDelegate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PlayQueueItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent) {}
|
|
||||||
|
|
||||||
Wt::WWidget* update(Wt::WWidget *widget, const Wt::WModelIndex &index, Wt::WFlags< Wt::ViewItemRenderFlag > flags)
|
|
||||||
{
|
|
||||||
Wt::WWidget* res;
|
|
||||||
|
|
||||||
if (!index.data(TrackInfoRole).empty())
|
|
||||||
{
|
|
||||||
TrackInfo trackInfo = boost::any_cast<TrackInfo>(index.data(TrackInfoRole));
|
|
||||||
|
|
||||||
Wt::WContainerWidget *container = new Wt::WContainerWidget();
|
|
||||||
Wt::WText* track = new Wt::WText(trackInfo.track, Wt::PlainText, container);
|
|
||||||
Wt::WText* artist = new Wt::WText(trackInfo.artist + " - " + trackInfo.release, Wt::PlainText, container);
|
|
||||||
|
|
||||||
artist->setInline(false);
|
|
||||||
track->setInline(false);
|
|
||||||
|
|
||||||
artist->setStyleClass("playqueue-artist");
|
|
||||||
track->setStyleClass("playqueue-track");
|
|
||||||
|
|
||||||
// Apply style if any
|
|
||||||
Wt::WString styleClass = Wt::asString(index.data(Wt::StyleClassRole));
|
|
||||||
|
|
||||||
// Apply selection style if any
|
|
||||||
if (flags & Wt::RenderSelected)
|
|
||||||
styleClass += " " + Wt::WApplication::instance()->theme()->activeClass();
|
|
||||||
|
|
||||||
container->setStyleClass(styleClass);
|
|
||||||
|
|
||||||
res = container;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
res = Wt::WItemDelegate::update(widget, index, flags);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
PlayQueue::PlayQueue(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WTableView(parent),
|
|
||||||
_curPlayedTrackPos(trackPosInvalid),
|
|
||||||
_trackSelector(new TrackSelector())
|
|
||||||
{
|
|
||||||
_model = new Wt::WStandardItemModel(0, 3, this);
|
|
||||||
|
|
||||||
// 0 Column is hidden (track id)
|
|
||||||
|
|
||||||
this->setHeaderHeight(0);
|
|
||||||
this->setModel(_model);
|
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
|
||||||
this->setSortingEnabled(false);
|
|
||||||
this->setAlternatingRowColors(true);
|
|
||||||
this->setRowHeight(64);
|
|
||||||
this->setColumnWidth(COLUMN_ID_COVER, 64);
|
|
||||||
this->setColumnWidth(COLUMN_ID_NAME, 240);
|
|
||||||
|
|
||||||
this->setLayoutSizeAware(true);
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
|
||||||
#endif
|
|
||||||
this->setColumnHidden(COLUMN_ID_TRACK_ID, true);
|
|
||||||
|
|
||||||
_itemDelegate = new PlayQueueItemDelegate();
|
|
||||||
this->setItemDelegate(_itemDelegate);
|
|
||||||
|
|
||||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
|
||||||
{
|
|
||||||
if (!idx.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Update selection
|
|
||||||
Wt::WModelIndexSet indexSet;
|
|
||||||
indexSet.insert(idx);
|
|
||||||
|
|
||||||
this->setSelectedIndexes( indexSet );
|
|
||||||
|
|
||||||
// Read the requested track
|
|
||||||
play(idx.row());
|
|
||||||
|
|
||||||
}, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::layoutSizeChanged (int width, int height)
|
|
||||||
{
|
|
||||||
std::size_t coverColumnSize = this->columnWidth(COLUMN_ID_COVER).toPixels();
|
|
||||||
// Set the remaining size for the name column
|
|
||||||
this->setColumnWidth(COLUMN_ID_NAME, width - coverColumnSize - (7 * 2) - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::setShuffle(bool enable)
|
|
||||||
{
|
|
||||||
_trackSelector->setShuffle(enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::setLoop(bool enable)
|
|
||||||
{
|
|
||||||
_trackSelector->setLoop(enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::play()
|
|
||||||
{
|
|
||||||
_trackSelector->setPos(0);
|
|
||||||
|
|
||||||
readTrack(_trackSelector->getCurrent());
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::play(int rowId)
|
|
||||||
{
|
|
||||||
// Update the track selector to use the requested track as current position
|
|
||||||
_trackSelector->setPosByRowId(rowId);
|
|
||||||
|
|
||||||
readTrack(_trackSelector->getCurrent());
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::select(int rowId)
|
|
||||||
{
|
|
||||||
// Update the track selector to use the requested track as current position
|
|
||||||
_trackSelector->setPosByRowId(rowId);
|
|
||||||
|
|
||||||
Wt::WTableView::select(_model->index(_trackSelector->getCurrent(), 0));
|
|
||||||
|
|
||||||
this->scrollTo( _model->index(_trackSelector->getCurrent(), 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::addTracks(const std::vector<Database::Track::id_type>& trackIds)
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Adding " << trackIds.size() << " tracks to play queue";
|
|
||||||
|
|
||||||
// Add tracks to model
|
|
||||||
for (Track::id_type trackId : trackIds)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Track::pointer track (Track::getById(DboSession(), trackId));
|
|
||||||
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
int dataRow = _model->rowCount();
|
|
||||||
|
|
||||||
_model->insertRows(dataRow, 1);
|
|
||||||
|
|
||||||
_model->setData(dataRow, COLUMN_ID_TRACK_ID, track.id(), Wt::UserRole);
|
|
||||||
|
|
||||||
std::string coverUrl;
|
|
||||||
coverUrl = SessionImageResource()->getTrackUrl(track.id(), 64);
|
|
||||||
|
|
||||||
_model->setData(dataRow, COLUMN_ID_COVER, coverUrl, Wt::DecorationRole);
|
|
||||||
_model->setData(dataRow, COLUMN_ID_COVER, std::string("playqueue-cover"), Wt::StyleClassRole);
|
|
||||||
|
|
||||||
TrackInfo trackInfo;
|
|
||||||
trackInfo.track = Wt::WString::fromUTF8(track->getName());
|
|
||||||
trackInfo.artist = Wt::WString::fromUTF8(track->getArtist()->getName());
|
|
||||||
trackInfo.release = Wt::WString::fromUTF8(track->getRelease()->getName());
|
|
||||||
_model->setData(dataRow, COLUMN_ID_NAME, trackInfo, TrackInfoRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackSelector->setSize( _model->rowCount() );
|
|
||||||
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::clear(void)
|
|
||||||
{
|
|
||||||
_model->removeRows(0, _model->rowCount());
|
|
||||||
|
|
||||||
// Reset play id
|
|
||||||
_curPlayedTrackPos = trackPosInvalid;
|
|
||||||
_trackSelector->setSize( 0 );
|
|
||||||
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::handlePlaybackComplete(void)
|
|
||||||
{
|
|
||||||
playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::playNext(void)
|
|
||||||
{
|
|
||||||
std::size_t nbTries = _trackSelector->getSize();
|
|
||||||
|
|
||||||
while (nbTries > 0)
|
|
||||||
{
|
|
||||||
int pos = _trackSelector->next();
|
|
||||||
if (pos == trackPosInvalid)
|
|
||||||
break;
|
|
||||||
|
|
||||||
readTrack(pos);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::playPrevious(void)
|
|
||||||
{
|
|
||||||
std::size_t nbTries = _trackSelector->getSize();
|
|
||||||
|
|
||||||
while (nbTries > 0)
|
|
||||||
{
|
|
||||||
int pos = _trackSelector->previous();
|
|
||||||
if (pos == trackPosInvalid)
|
|
||||||
break;
|
|
||||||
|
|
||||||
readTrack(pos);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::readTrack(int rowPos)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Reading track at pos " << rowPos << ", row count = " << _model->rowCount();
|
|
||||||
|
|
||||||
if (rowPos < _model->rowCount())
|
|
||||||
{
|
|
||||||
Database::Track::id_type trackId = boost::any_cast<Database::Track::id_type>(_model->data(rowPos, COLUMN_ID_TRACK_ID, Wt::UserRole));
|
|
||||||
|
|
||||||
setPlayingTrackPos(rowPos);
|
|
||||||
|
|
||||||
_sigTrackPlay.emit(trackId, rowPos);
|
|
||||||
|
|
||||||
this->scrollTo( _model->index(_trackSelector->getCurrent(), 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::setPlayingTrackPos(int newRowPos)
|
|
||||||
{
|
|
||||||
int oldRowPos = _curPlayedTrackPos;
|
|
||||||
_curPlayedTrackPos = newRowPos;
|
|
||||||
|
|
||||||
// Hack re-set the data in order to trigger the rerending of the widget
|
|
||||||
// calling the update method of our custom item delegate gives bad results
|
|
||||||
if (oldRowPos >= 0)
|
|
||||||
{
|
|
||||||
_model->setData(oldRowPos, COLUMN_ID_NAME, boost::any(), Wt::StyleClassRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRowPos >= 0)
|
|
||||||
{
|
|
||||||
_model->setData(newRowPos, COLUMN_ID_NAME, std::string("playqueue-playing"), Wt::StyleClassRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::delSelected(void)
|
|
||||||
{
|
|
||||||
int minId = _model->rowCount();
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
// Make sure to delete entries in the reverse order
|
|
||||||
std::vector<int> rowIds;
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
rowIds.push_back(index.row());
|
|
||||||
|
|
||||||
std::sort(rowIds.begin(), rowIds.end(), std::greater<int>());
|
|
||||||
if (!rowIds.empty())
|
|
||||||
minId = rowIds.back();
|
|
||||||
|
|
||||||
for (int rowId : rowIds)
|
|
||||||
_model->removeRow(rowId);
|
|
||||||
|
|
||||||
// If the current played track is removed, make sure to unselect it
|
|
||||||
_trackSelector->setSize(_model->rowCount());
|
|
||||||
|
|
||||||
renumber(minId, _model->rowCount() - 1);
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::delAll(void)
|
|
||||||
{
|
|
||||||
_model->removeRows(0, _model->rowCount());
|
|
||||||
_trackSelector->setSize(0);
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::moveSelectedUp(void)
|
|
||||||
{
|
|
||||||
int minId = _model->rowCount();
|
|
||||||
int maxId = 0;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
Wt::WModelIndexSet newIndexSet;
|
|
||||||
|
|
||||||
// ordered from up to down
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
// Do nothing if the first selected index is on the top
|
|
||||||
if (index.row() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO optimize for blocks
|
|
||||||
swapRows(_model, index.row() - 1, index.row());
|
|
||||||
|
|
||||||
if (index.row() - 1 < minId)
|
|
||||||
minId = index.row() - 1;
|
|
||||||
|
|
||||||
if (index.row() > maxId)
|
|
||||||
maxId = index.row();
|
|
||||||
|
|
||||||
// Update playing status
|
|
||||||
if (_curPlayedTrackPos == index.row())
|
|
||||||
setPlayingTrackPos(_curPlayedTrackPos - 1);
|
|
||||||
else if (_curPlayedTrackPos == index.row() - 1)
|
|
||||||
setPlayingTrackPos(_curPlayedTrackPos + 1);
|
|
||||||
|
|
||||||
newIndexSet.insert( _model->index(index.row() - 1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackSelector->setPosByRowId(_curPlayedTrackPos);
|
|
||||||
this->setSelectedIndexes( newIndexSet );
|
|
||||||
|
|
||||||
renumber(minId, maxId);
|
|
||||||
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::moveSelectedDown(void)
|
|
||||||
{
|
|
||||||
int minId = _model->rowCount();
|
|
||||||
int maxId = 0;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
Wt::WModelIndexSet newIndexSet;
|
|
||||||
|
|
||||||
// ordered from up to down
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
// Do nothing if the last selected index is the last one
|
|
||||||
if (index.row() == _model->rowCount() - 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO optimize for blocks
|
|
||||||
swapRows(_model, index.row(), index.row() + 1);
|
|
||||||
|
|
||||||
if (index.row() < minId)
|
|
||||||
minId = index.row();
|
|
||||||
|
|
||||||
if (index.row() + 1 > maxId)
|
|
||||||
maxId = index.row() + 1;
|
|
||||||
|
|
||||||
// Update playing status
|
|
||||||
if (_curPlayedTrackPos == index.row())
|
|
||||||
setPlayingTrackPos(_curPlayedTrackPos + 1);
|
|
||||||
else if (_curPlayedTrackPos == index.row() + 1)
|
|
||||||
setPlayingTrackPos(_curPlayedTrackPos - 1);
|
|
||||||
|
|
||||||
newIndexSet.insert( _model->index(index.row() + 1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackSelector->setPosByRowId(_curPlayedTrackPos);
|
|
||||||
this->setSelectedIndexes( newIndexSet );
|
|
||||||
|
|
||||||
renumber(minId, maxId);
|
|
||||||
|
|
||||||
_sigTracksUpdated.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::renumber(int firstId, int lastId)
|
|
||||||
{
|
|
||||||
for (int i = firstId; i <= lastId; ++i)
|
|
||||||
_model->setData( i, 1, i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::getTracks(std::vector<Database::Track::id_type>& trackIds) const
|
|
||||||
{
|
|
||||||
// Now add each entry in the playlist
|
|
||||||
for (int i = 0; i < _model->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
Database::Track::id_type trackId = boost::any_cast<Database::Track::id_type>(_model->data(i, COLUMN_ID_TRACK_ID, Wt::UserRole));
|
|
||||||
trackIds.push_back(trackId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef PLAY_QUEUE_HPP
|
|
||||||
#define PLAY_QUEUE_HPP
|
|
||||||
|
|
||||||
#include <Wt/WTableView>
|
|
||||||
#include <Wt/WStandardItemModel>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class PlayQueueItemDelegate;
|
|
||||||
class TrackSelector;
|
|
||||||
|
|
||||||
class PlayQueue : public Wt::WTableView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PlayQueue(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
void addTracks(const std::vector<Database::Track::id_type>& trackIds);
|
|
||||||
void getTracks(std::vector<Database::Track::id_type>& trackIds) const;
|
|
||||||
|
|
||||||
void clear(void);
|
|
||||||
|
|
||||||
void setShuffle(bool enable); // true to enable shuffle
|
|
||||||
void setLoop(bool enable); // true to enable loop
|
|
||||||
|
|
||||||
// Play functions
|
|
||||||
void play(void); // Play the queue from the beginning
|
|
||||||
void play(int rowId); // Play the queue from the given rowId
|
|
||||||
void select(int rowId); // Select the given rowId
|
|
||||||
void playNext(void); // Play the next track
|
|
||||||
void playPrevious(void); // Play the previous track
|
|
||||||
|
|
||||||
// List manipulations
|
|
||||||
void delSelected(void);
|
|
||||||
void delAll(void);
|
|
||||||
void moveSelectedUp(void);
|
|
||||||
void moveSelectedDown(void);
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
// Emitted when a song has to be played
|
|
||||||
Wt::Signal< Database::Track::id_type, int >& playTrack() { return _sigTrackPlay; }
|
|
||||||
// Emitted when the list has changed
|
|
||||||
Wt::Signal< void >& tracksUpdated() { return _sigTracksUpdated; }
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
void handlePlaybackComplete(void);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void layoutSizeChanged (int width, int height);
|
|
||||||
|
|
||||||
void readTrack(int rowId);
|
|
||||||
void setPlayingTrackPos(int newRowPos);
|
|
||||||
void renumber(int firstId, int lastId);
|
|
||||||
|
|
||||||
Wt::Signal< Database::Track::id_type, int > _sigTrackPlay;
|
|
||||||
Wt::Signal< void > _sigTracksUpdated;
|
|
||||||
|
|
||||||
Wt::WStandardItemModel* _model;
|
|
||||||
|
|
||||||
PlayQueueItemDelegate* _itemDelegate;
|
|
||||||
|
|
||||||
int _curPlayedTrackPos;
|
|
||||||
std::unique_ptr<TrackSelector> _trackSelector;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WConfig.h>
|
|
||||||
#include <Wt/WItemDelegate>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "TableFilter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
TableFilterCluster::TableFilterCluster(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WTableView( parent ), Filter()
|
|
||||||
{
|
|
||||||
const std::vector<Wt::WString> columnNames = {"Tag", "Tracks"};
|
|
||||||
|
|
||||||
SearchFilter filter;
|
|
||||||
|
|
||||||
Cluster::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
|
||||||
|
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
|
||||||
this->setSortingEnabled(true);
|
|
||||||
this->setAlternatingRowColors(true);
|
|
||||||
this->setModel(&_queryModel);
|
|
||||||
|
|
||||||
this->setColumnWidth(1, 80);
|
|
||||||
|
|
||||||
this->selectionChanged().connect(this, &TableFilterCluster::emitUpdate);
|
|
||||||
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this->setLayoutSizeAware(true);
|
|
||||||
|
|
||||||
_queryModel.setBatchSize(150);
|
|
||||||
|
|
||||||
// If an item is double clicked, select and emit signal
|
|
||||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
|
||||||
{
|
|
||||||
if (!idx.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet;
|
|
||||||
indexSet.insert(idx);
|
|
||||||
|
|
||||||
this->setSelectedIndexes( indexSet );
|
|
||||||
_sigDoubleClicked.emit( );
|
|
||||||
|
|
||||||
}, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TableFilterCluster::layoutSizeChanged (int width, int height)
|
|
||||||
{
|
|
||||||
std::size_t trackColumnSize = this->columnWidth(1).toPixels();
|
|
||||||
// Set the remaining size for the name column
|
|
||||||
this->setColumnWidth(0, width - trackColumnSize - (7 * 2) - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void
|
|
||||||
TableFilterCluster::refresh(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
#if WT_VERSION >= 0X03030500
|
|
||||||
this->clearSelection();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Cluster::updateUIQueryModel(DboSession(), _queryModel, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get constraint created by this filter
|
|
||||||
void
|
|
||||||
TableFilterCluster::getConstraint(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// TODO set invisible track id
|
|
||||||
Cluster::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
|
||||||
|
|
||||||
filter.idMatch[Database::SearchFilter::Field::Cluster].push_back(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableFilterArtist::TableFilterArtist(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WTableView( parent ), Filter()
|
|
||||||
{
|
|
||||||
const std::vector<Wt::WString> columnNames = {"Artist", "Releases", "Tracks"};
|
|
||||||
|
|
||||||
SearchFilter filter;
|
|
||||||
|
|
||||||
Artist::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
|
||||||
|
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
|
||||||
this->setSortingEnabled(true);
|
|
||||||
this->setAlternatingRowColors(true);
|
|
||||||
this->setModel(&_queryModel);
|
|
||||||
|
|
||||||
this->setColumnWidth(1, 80);
|
|
||||||
this->setColumnWidth(2, 80);
|
|
||||||
|
|
||||||
this->selectionChanged().connect(this, &TableFilterArtist::emitUpdate);
|
|
||||||
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this->setLayoutSizeAware(true);
|
|
||||||
|
|
||||||
_queryModel.setBatchSize(150);
|
|
||||||
|
|
||||||
// If an item is double clicked, select and emit signal
|
|
||||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
|
||||||
{
|
|
||||||
if (!idx.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet;
|
|
||||||
indexSet.insert(idx);
|
|
||||||
|
|
||||||
this->setSelectedIndexes( indexSet );
|
|
||||||
_sigDoubleClicked.emit( );
|
|
||||||
|
|
||||||
}, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TableFilterArtist::layoutSizeChanged (int width, int height)
|
|
||||||
{
|
|
||||||
std::size_t otherColumnsSize = this->columnWidth(1).toPixels() + this->columnWidth(2).toPixels();
|
|
||||||
// Set the remaining size for the name column
|
|
||||||
this->setColumnWidth(0, width - otherColumnsSize - (7 * 3) - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void
|
|
||||||
TableFilterArtist::refresh(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
#if WT_VERSION >= 0X03030500
|
|
||||||
this->clearSelection();
|
|
||||||
#endif
|
|
||||||
Artist::updateUIQueryModel(DboSession(), _queryModel, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get constraint created by this filter
|
|
||||||
void
|
|
||||||
TableFilterArtist::getConstraint(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Artist::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
|
||||||
|
|
||||||
filter.idMatch[SearchFilter::Field::Artist].push_back(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TableFilterRelease::TableFilterRelease(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WTableView( parent ), Filter()
|
|
||||||
{
|
|
||||||
const std::vector<Wt::WString> columnNames = {"Release", "Date", "Tracks"};
|
|
||||||
|
|
||||||
SearchFilter filter;
|
|
||||||
|
|
||||||
Release::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
|
||||||
|
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
|
||||||
this->setSortingEnabled(true);
|
|
||||||
this->setAlternatingRowColors(true);
|
|
||||||
this->setModel(&_queryModel);
|
|
||||||
|
|
||||||
this->setColumnWidth(1, 60);
|
|
||||||
this->setColumnWidth(2, 80);
|
|
||||||
|
|
||||||
// Date display, just the year
|
|
||||||
{
|
|
||||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
|
||||||
delegate->setTextFormat("yyyy");
|
|
||||||
this->setItemDelegateForColumn(1, delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->selectionChanged().connect(this, &TableFilterRelease::emitUpdate);
|
|
||||||
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal);
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this->setLayoutSizeAware(true);
|
|
||||||
|
|
||||||
_queryModel.setBatchSize(150);
|
|
||||||
|
|
||||||
// If an item is double clicked, select and emit signal
|
|
||||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
|
||||||
{
|
|
||||||
if (!idx.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet;
|
|
||||||
indexSet.insert(idx);
|
|
||||||
|
|
||||||
this->setSelectedIndexes( indexSet );
|
|
||||||
_sigDoubleClicked.emit( );
|
|
||||||
|
|
||||||
}, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TableFilterRelease::layoutSizeChanged (int width, int height)
|
|
||||||
{
|
|
||||||
std::size_t otherColumnSizes = this->columnWidth(1).toPixels() + this->columnWidth(2).toPixels();
|
|
||||||
// Set the remaining size for the name column
|
|
||||||
this->setColumnWidth(0, width - otherColumnSizes - (7 * 3) - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void
|
|
||||||
TableFilterRelease::refresh(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
#if WT_VERSION >= 0X03030500
|
|
||||||
this->clearSelection();
|
|
||||||
#endif
|
|
||||||
Release::updateUIQueryModel(DboSession(), _queryModel, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get constraint created by this filter
|
|
||||||
void
|
|
||||||
TableFilterRelease::getConstraint(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Release::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
|
||||||
|
|
||||||
filter.idMatch[Database::SearchFilter::Field::Release].push_back(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef TABLE_FILTER_HPP
|
|
||||||
#define TABLE_FILTER_HPP
|
|
||||||
|
|
||||||
|
|
||||||
#include <Wt/Dbo/QueryModel>
|
|
||||||
#include <Wt/WTableView>
|
|
||||||
|
|
||||||
#include "Filter.hpp"
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class TableFilterCluster : public Wt::WTableView, public Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TableFilterCluster(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void refresh(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
// Get constraints created by this filter
|
|
||||||
void getConstraint(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
void layoutSizeChanged (int width, int height);
|
|
||||||
|
|
||||||
typedef Wt::Signal<void> SigDoubleClicked;
|
|
||||||
|
|
||||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
SigDoubleClicked _sigDoubleClicked;
|
|
||||||
|
|
||||||
Wt::Dbo::QueryModel<Database::Cluster::UIQueryResult> _queryModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TableFilterArtist : public Wt::WTableView, public Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TableFilterArtist(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void refresh(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
// Get constraints created by this filter
|
|
||||||
void getConstraint(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
void layoutSizeChanged (int width, int height);
|
|
||||||
|
|
||||||
typedef Wt::Signal<void> SigDoubleClicked;
|
|
||||||
|
|
||||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
SigDoubleClicked _sigDoubleClicked;
|
|
||||||
|
|
||||||
// Name, track count
|
|
||||||
Wt::Dbo::QueryModel<Database::Artist::UIQueryResult> _queryModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TableFilterRelease : public Wt::WTableView, public Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TableFilterRelease(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
// Set constraints on this filter
|
|
||||||
void refresh(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
// Get constraints created by this filter
|
|
||||||
void getConstraint(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
void layoutSizeChanged (int width, int height);
|
|
||||||
|
|
||||||
typedef Wt::Signal<void> SigDoubleClicked;
|
|
||||||
|
|
||||||
SigDoubleClicked& sigDoubleClicked() { return _sigDoubleClicked; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
SigDoubleClicked _sigDoubleClicked;
|
|
||||||
|
|
||||||
// Name, track count
|
|
||||||
Wt::Dbo::QueryModel<Database::Release::UIQueryResult> _queryModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/date_time.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WConfig.h>
|
|
||||||
#include <Wt/WItemDelegate>
|
|
||||||
#include <Wt/WBreak>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "TrackView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
class DurationItemDelegate : public Wt::WItemDelegate
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
DurationItemDelegate(Wt::WObject *parent = 0) : Wt::WItemDelegate(parent) {}
|
|
||||||
|
|
||||||
Wt::WWidget* update(Wt::WWidget *widget, const Wt::WModelIndex &index, Wt::WFlags< Wt::ViewItemRenderFlag > flags)
|
|
||||||
{
|
|
||||||
boost::posix_time::time_duration duration = boost::any_cast<boost::posix_time::time_duration>(index.data(Wt::DisplayRole));
|
|
||||||
|
|
||||||
// TODO bug when redrawn
|
|
||||||
std::string format = duration.total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
|
||||||
|
|
||||||
return new Wt::WText(durationToString(duration, format), Wt::PlainText);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TrackView::TrackView(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WTableView( parent )
|
|
||||||
{
|
|
||||||
|
|
||||||
static const std::vector<Wt::WString> columnNames =
|
|
||||||
{
|
|
||||||
"Artist",
|
|
||||||
"Album",
|
|
||||||
"Disc #",
|
|
||||||
"Track #",
|
|
||||||
"Track",
|
|
||||||
"Duration",
|
|
||||||
"Date",
|
|
||||||
"Original Date",
|
|
||||||
"Genres",
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchFilter filter;
|
|
||||||
|
|
||||||
Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames);
|
|
||||||
|
|
||||||
_queryModel.setBatchSize(500);
|
|
||||||
|
|
||||||
this->setSortingEnabled(true);
|
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
|
||||||
this->setAlternatingRowColors(true);
|
|
||||||
this->setModel(&_queryModel);
|
|
||||||
|
|
||||||
this->setColumnWidth(0, 180); // Artist
|
|
||||||
this->setColumnWidth(1, 180); // Album
|
|
||||||
this->setColumnWidth(2, 70); // Disc Number
|
|
||||||
this->setColumnWidth(3, 70); // Track Number
|
|
||||||
this->setColumnWidth(4, 180); // Track
|
|
||||||
this->setColumnWidth(5, 70); // Duration
|
|
||||||
this->setColumnWidth(6, 70); // Date
|
|
||||||
this->setColumnWidth(7, 70); // Original Date
|
|
||||||
this->setColumnWidth(8, 180); // Genres
|
|
||||||
#if WT_VERSION >= 0X03030400
|
|
||||||
this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Duration display
|
|
||||||
{
|
|
||||||
// TODO better handle 1 hour+ files!
|
|
||||||
DurationItemDelegate *delegate = new DurationItemDelegate(this);
|
|
||||||
this->setItemDelegateForColumn(5, delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date display, just the year
|
|
||||||
{
|
|
||||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
|
||||||
delegate->setTextFormat("yyyy");
|
|
||||||
this->setItemDelegateForColumn(6, delegate);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Wt::WItemDelegate *delegate = new Wt::WItemDelegate(this);
|
|
||||||
delegate->setTextFormat("yyyy");
|
|
||||||
this->setItemDelegateForColumn(7, delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an item is double clicked, select the track and emit signal
|
|
||||||
this->doubleClicked().connect( std::bind([=] (Wt::WModelIndex idx, Wt::WMouseEvent evt)
|
|
||||||
{
|
|
||||||
if (!idx.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet;
|
|
||||||
indexSet.insert(idx);
|
|
||||||
|
|
||||||
this->setSelectedIndexes( indexSet );
|
|
||||||
|
|
||||||
_sigTrackDoubleClicked.emit( );
|
|
||||||
|
|
||||||
}, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackView::emitStats(const SearchFilter& filter)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction (DboSession());
|
|
||||||
|
|
||||||
// Update stats on the view
|
|
||||||
Track::StatsQueryResult stats = Track::getStats(DboSession(), filter);
|
|
||||||
|
|
||||||
transaction.commit();
|
|
||||||
|
|
||||||
int nbTracks = stats.get<0>();
|
|
||||||
boost::posix_time::time_duration totalDuration = stats.get<1>();
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
|
|
||||||
oss << nbTracks << " track" << (nbTracks > 1 ? "s" : "") << ", ";
|
|
||||||
|
|
||||||
if (totalDuration.hours() >= 24)
|
|
||||||
{
|
|
||||||
auto days = totalDuration.hours() / 24;
|
|
||||||
oss << days << " day" << (days > 1 ? "s " : " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << std::setw(2) << std::setfill('0') << totalDuration.hours() % 24
|
|
||||||
<< ":" << std::setw(2) << std::setfill('0') << totalDuration.minutes()
|
|
||||||
<< ":" << std::setw(2) << std::setfill('0') << totalDuration.seconds();
|
|
||||||
|
|
||||||
_sigStatsUpdated.emit(Wt::WString(oss.str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set constraints created by parent filters
|
|
||||||
void
|
|
||||||
TrackView::refresh(SearchFilter& filter)
|
|
||||||
{
|
|
||||||
#if WT_VERSION >= 0X03030500
|
|
||||||
this->clearSelection();
|
|
||||||
#endif
|
|
||||||
Track::updateUIQueryModel(DboSession(), _queryModel, filter);
|
|
||||||
|
|
||||||
emitStats(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackView::getSelectedTracks(std::vector<Track::id_type>& track_ids)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Getting selected tracks...";
|
|
||||||
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Track::id_type id = _queryModel.resultRow( index.row() ).get<0>();
|
|
||||||
|
|
||||||
track_ids.push_back(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Getting all selected tracks: " << track_ids.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
|
||||||
TrackView::getNbSelectedTracks(void)
|
|
||||||
{
|
|
||||||
return this->selectedIndexes().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
TrackView::getFirstSelectedTrackPosition(void)
|
|
||||||
{
|
|
||||||
Wt::WModelIndexSet indexSet = this->selectedIndexes();
|
|
||||||
|
|
||||||
for (Wt::WModelIndex index : indexSet)
|
|
||||||
{
|
|
||||||
if (!index.isValid())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return index.row();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackView::getTracks(std::vector<Track::id_type>& trackIds)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Getting all tracks...";
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
Wt::Dbo::Query<Track::UIQueryResult> query = _queryModel.query();
|
|
||||||
Wt::Dbo::collection<Track::UIQueryResult> results = query.limit(-1).offset(-1);
|
|
||||||
|
|
||||||
for (auto it = results.begin(); it != results.end(); ++it)
|
|
||||||
{
|
|
||||||
Track::id_type id = it->get<0>();
|
|
||||||
trackIds.push_back(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Getting all tracks done! " << trackIds.size() << " tracks!";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef TRACK_HPP
|
|
||||||
#define TRACK_HPP
|
|
||||||
|
|
||||||
#include <Wt/WTableView>
|
|
||||||
#include <Wt/Dbo/QueryModel>
|
|
||||||
|
|
||||||
#include "database/DatabaseHandler.hpp"
|
|
||||||
|
|
||||||
#include "Filter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class TrackView : public Wt::WTableView, public Filter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
TrackView(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
// Filter interface
|
|
||||||
// Set constraints created by parent filters
|
|
||||||
void refresh(Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
// Create constraints for child filters (N/A)
|
|
||||||
void getConstraint(Database::SearchFilter& filter) {}
|
|
||||||
|
|
||||||
// Get all the tracks that are currently selected
|
|
||||||
void getSelectedTracks(std::vector<Database::Track::id_type>& trackIds);
|
|
||||||
|
|
||||||
std::size_t getNbSelectedTracks(void);
|
|
||||||
|
|
||||||
// Get the first position of the selected tracks (0 if nothing selected)
|
|
||||||
int getFirstSelectedTrackPosition(void);
|
|
||||||
|
|
||||||
// Get all the tracks
|
|
||||||
void getTracks(std::vector<Database::Track::id_type>& track_ids);
|
|
||||||
|
|
||||||
typedef Wt::Signal<void> SigTrackDoubleClicked;
|
|
||||||
typedef Wt::Signal<Wt::WString> SigStatsUpdated;
|
|
||||||
|
|
||||||
SigTrackDoubleClicked& trackDoubleClicked() { return _sigTrackDoubleClicked; }
|
|
||||||
SigStatsUpdated& statsUpdated() { return _sigStatsUpdated; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
SigTrackDoubleClicked _sigTrackDoubleClicked;
|
|
||||||
SigStatsUpdated _sigStatsUpdated;
|
|
||||||
|
|
||||||
void emitStats(const Database::SearchFilter& filter);
|
|
||||||
|
|
||||||
typedef Database::Track::UIQueryResult ResultType;
|
|
||||||
Wt::Dbo::QueryModel< ResultType > _queryModel;
|
|
||||||
Wt::WTableView* _tableView;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "ArtistSearch.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
ArtistSearch::ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
|
||||||
t->setTemplateText(Wt::WString::tr("wa-artist-search"));
|
|
||||||
|
|
||||||
Wt::WTemplate *titleTemplate = new Wt::WTemplate(this);
|
|
||||||
titleTemplate->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
|
||||||
titleTemplate->bindString("text", title);
|
|
||||||
t->bindWidget("title", titleTemplate);
|
|
||||||
|
|
||||||
_contents = new Wt::WContainerWidget();
|
|
||||||
t->bindWidget("contents", _contents );
|
|
||||||
|
|
||||||
_showMore = new Wt::WTemplate();
|
|
||||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
|
||||||
_showMore->bindString("text", "Tap to show more results...");
|
|
||||||
_showMore->hide();
|
|
||||||
_showMore->clicked().connect(std::bind([=] {
|
|
||||||
_sigShowMore.emit();
|
|
||||||
}));
|
|
||||||
t->bindWidget("show-more", _showMore);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistSearch::clear()
|
|
||||||
{
|
|
||||||
_contents->clear();
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistSearch::search(Database::SearchFilter filter, size_t nb)
|
|
||||||
{
|
|
||||||
_filter = filter;
|
|
||||||
|
|
||||||
clear();
|
|
||||||
addResults(nb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ArtistSearch::addResults(std::size_t nb)
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
bool moreResults;
|
|
||||||
std::vector<Artist::pointer> artists = Artist::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
|
||||||
|
|
||||||
for (Artist::pointer artist : artists)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
|
||||||
res->setTemplateText(Wt::WString::tr("wa-artist-search-res"));
|
|
||||||
|
|
||||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt:: WLink::InternalPath, "/audio/artist/" + std::to_string(artist.id())));
|
|
||||||
Wt::WImage *artistImg = new Wt::WImage(coverAnchor);
|
|
||||||
artistImg->setImageLink( SessionImageResource()->getArtistUrl(artist.id(), 512));
|
|
||||||
artistImg->setStyleClass("center-block"); // TODO move in css?
|
|
||||||
artistImg->setStyleClass("release_res_shadow release_img-responsive"); // TODO move in css?
|
|
||||||
|
|
||||||
res->bindWidget("gif", coverAnchor);
|
|
||||||
res->bindString("name", Wt::WString::fromUTF8(artist->getName(), Wt::PlainText));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moreResults)
|
|
||||||
_showMore->show();
|
|
||||||
else
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_MOBILE_ARTIST_SEARCH_HPP
|
|
||||||
#define UI_MOBILE_ARTIST_SEARCH_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
#include "database/SearchFilter.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class ArtistSearch : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void search(Database::SearchFilter filter, std::size_t nb);
|
|
||||||
void addResults(std::size_t nb);
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigShowMore;
|
|
||||||
|
|
||||||
void clear(void);
|
|
||||||
|
|
||||||
Wt::WTemplate* _showMore;
|
|
||||||
|
|
||||||
Database::SearchFilter _filter;
|
|
||||||
Wt::WContainerWidget* _contents;
|
|
||||||
std::size_t _count;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "SearchUtils.hpp"
|
|
||||||
|
|
||||||
#include "ArtistSearch.hpp"
|
|
||||||
|
|
||||||
#include "ArtistSearchView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
#define SEARCH_NB_ITEMS 20
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
ArtistSearchView::ArtistSearchView(Wt::WContainerWidget* parent)
|
|
||||||
{
|
|
||||||
ArtistSearch* artistSearch = new ArtistSearch("Artists", this);
|
|
||||||
artistSearch->showMore().connect(std::bind([=] {
|
|
||||||
artistSearch->addResults(SEARCH_NB_ITEMS);
|
|
||||||
}));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/search/artist";
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
|
||||||
|
|
||||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseSearch.hpp"
|
|
||||||
#include "ArtistView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
ArtistView::ArtistView(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
ReleaseSearch *releases = new ReleaseSearch(this);
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/artist/";
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::string strId = path.substr(pathPrefix.length());
|
|
||||||
|
|
||||||
Artist::id_type id;
|
|
||||||
if (readAs(strId, id))
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
auto artist = Artist::getById(DboSession(), id);
|
|
||||||
releases->search(SearchFilter::ById(SearchFilter::Field::Artist, id), 20, artist ? Wt::WString::fromUTF8(artist->getName()) : "Unknown artist");
|
|
||||||
}
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WLink
|
|
||||||
ArtistView::getLink(Database::Artist::id_type id)
|
|
||||||
{
|
|
||||||
return Wt::WLink(Wt::WLink::InternalPath, "/audio/artist/" + std::to_string(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
} //namespace Mobile
|
|
||||||
} //namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include <Wt/WStackedWidget>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
#include "audio/AudioPlayer.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "PreviewSearchView.hpp"
|
|
||||||
#include "ArtistSearchView.hpp"
|
|
||||||
#include "ReleaseSearchView.hpp"
|
|
||||||
#include "TrackSearchView.hpp"
|
|
||||||
|
|
||||||
#include "ArtistView.hpp"
|
|
||||||
#include "ReleaseView.hpp"
|
|
||||||
|
|
||||||
#include "MobilePlayQueue.hpp"
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "MobileAudio.hpp"
|
|
||||||
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
enum WidgetIdx
|
|
||||||
{
|
|
||||||
WidgetIdxSearchPreview = 0,
|
|
||||||
WidgetIdxSearchArtist = 1,
|
|
||||||
WidgetIdxSearchRelease = 2,
|
|
||||||
WidgetIdxSearchTrack = 3,
|
|
||||||
WidgetIdxArtist = 4,
|
|
||||||
WidgetIdxRelease = 5,
|
|
||||||
WidgetIdxPlayQueue = 6,
|
|
||||||
};
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
void
|
|
||||||
Audio::search(std::string text)
|
|
||||||
{
|
|
||||||
wApp->setInternalPath("/audio/search/preview/" + stringToUTF8(text), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Audio::Audio(Wt::WContainerWidget *parent)
|
|
||||||
: UserInterface::Audio(parent)
|
|
||||||
{
|
|
||||||
// Root div has to be a "container"
|
|
||||||
this->setStyleClass("container-fluid");
|
|
||||||
this->setPadding(60, Wt::Bottom);
|
|
||||||
|
|
||||||
Wt::WStackedWidget *stack = new Wt::WStackedWidget(this);
|
|
||||||
|
|
||||||
PlayQueue *playQueue = new PlayQueue();
|
|
||||||
|
|
||||||
// Same order as WidgetIdxXXX
|
|
||||||
stack->addWidget(new PreviewSearchView(_playQueueEvents));
|
|
||||||
stack->addWidget(new ArtistSearchView());
|
|
||||||
stack->addWidget(new ReleaseSearchView());
|
|
||||||
stack->addWidget(new TrackSearchView(_playQueueEvents));
|
|
||||||
stack->addWidget(new ArtistView());
|
|
||||||
stack->addWidget(new ReleaseView(_playQueueEvents));
|
|
||||||
stack->addWidget(playQueue);
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
// order is important: most specific path first
|
|
||||||
static std::map<std::string, int> indexes =
|
|
||||||
{
|
|
||||||
{ "/audio/search/preview", WidgetIdxSearchPreview},
|
|
||||||
{ "/audio/search/artist", WidgetIdxSearchArtist},
|
|
||||||
{ "/audio/search/release", WidgetIdxSearchRelease},
|
|
||||||
{ "/audio/search/track", WidgetIdxSearchTrack},
|
|
||||||
{ "/audio/artist", WidgetIdxArtist},
|
|
||||||
{ "/audio/release", WidgetIdxRelease},
|
|
||||||
{ "/audio/playqueue", WidgetIdxPlayQueue},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto index : indexes)
|
|
||||||
{
|
|
||||||
if (wApp->internalPathMatches(index.first))
|
|
||||||
stack->setCurrentIndex(index.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
|
|
||||||
Wt::WTemplate* footer = new Wt::WTemplate(this);
|
|
||||||
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
|
|
||||||
|
|
||||||
AudioPlayer* audioPlayer = new AudioPlayer(AudioPlayer::ControlPlayqueue);
|
|
||||||
footer->bindWidget("player", audioPlayer);
|
|
||||||
|
|
||||||
audioPlayer->showPlayQueue().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
wApp->setInternalPath("/audio/playqueue", true);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Connect the audio player events to the playqueue
|
|
||||||
playQueue->playTrack().connect(std::bind([=] (Database::Track::id_type id)
|
|
||||||
{
|
|
||||||
audioPlayer->loadTrack(id);
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
audioPlayer->playNext().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
playQueue->playNext();
|
|
||||||
}));
|
|
||||||
audioPlayer->playPrevious().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
playQueue->playPrevious();
|
|
||||||
}));
|
|
||||||
audioPlayer->playbackEnded().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
playQueue->playNext();
|
|
||||||
}));
|
|
||||||
audioPlayer->shuffle().connect(std::bind(&PlayQueue::setShuffle, playQueue, std::placeholders::_1));
|
|
||||||
audioPlayer->loop().connect(std::bind(&PlayQueue::setLoop, playQueue, std::placeholders::_1));
|
|
||||||
|
|
||||||
// Connect the events to the player
|
|
||||||
_playQueueEvents.trackPlay.connect(std::bind([=] (Database::Track::id_type id)
|
|
||||||
{
|
|
||||||
std::size_t pos = playQueue->addTrack(id);
|
|
||||||
playQueue->play(pos);
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
|
|
||||||
_playQueueEvents.trackAdd.connect(std::bind([=] (Database::Track::id_type id)
|
|
||||||
{
|
|
||||||
playQueue->addTrack(id);
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "MobilePlayQueue.hpp"
|
|
||||||
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
|
|
||||||
PlayQueue::PlayQueue(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
|
||||||
t->setTemplateText(Wt::WString::tr("wa-playqueue-view"));
|
|
||||||
|
|
||||||
_trackContainer = new Wt::WContainerWidget();
|
|
||||||
t->bindWidget("contents", _trackContainer);
|
|
||||||
|
|
||||||
Wt::WTemplate* controls = new Wt::WTemplate();
|
|
||||||
t->bindWidget("controls", controls);
|
|
||||||
controls->setTemplateText(Wt::WString::tr("wa-playqueue-controls"));
|
|
||||||
|
|
||||||
Wt::WText *loadBtn = new Wt::WText("<i class=\"fa fa-upload fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
controls->bindWidget("load", loadBtn);
|
|
||||||
loadBtn->setStyleClass("mobile-btn");
|
|
||||||
|
|
||||||
Wt::WText *saveBtn = new Wt::WText("<i class=\"fa fa-download fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
controls->bindWidget("save", saveBtn);
|
|
||||||
saveBtn->setStyleClass("mobile-btn");
|
|
||||||
|
|
||||||
Wt::WText *clearBtn = new Wt::WText("<i class=\"fa fa-remove fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
controls->bindWidget("clear", clearBtn);
|
|
||||||
clearBtn->setStyleClass("mobile-btn");
|
|
||||||
clearBtn->clicked().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
clear();
|
|
||||||
}));
|
|
||||||
|
|
||||||
_shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
controls->bindWidget("shuffle", _shuffleBtn);
|
|
||||||
_shuffleBtn->setStyleClass("mobile-btn");
|
|
||||||
_shuffleBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
setShuffle(!_shuffle);
|
|
||||||
}));
|
|
||||||
|
|
||||||
_loopBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
|
|
||||||
controls->bindWidget("repeat", _loopBtn);
|
|
||||||
_loopBtn->setStyleClass("mobile-btn");
|
|
||||||
_loopBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
setLoop(!_loop);
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
|
||||||
PlayQueue::addTrack(Database::Track::id_type id)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(DboSession(), id);
|
|
||||||
if (!track)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "No track found for id " << id;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t trackPos = _trackIds.size();
|
|
||||||
_trackIds.push_back(id);
|
|
||||||
|
|
||||||
Wt::WTemplate* t = new Wt::WTemplate(_trackContainer);
|
|
||||||
t->setTemplateText(Wt::WString::tr("wa-playqueue-track"));
|
|
||||||
|
|
||||||
Wt::WImage *cover = new Wt::WImage();
|
|
||||||
t->bindWidget("cover", cover);
|
|
||||||
cover->setStyleClass ("center-block img-responsive");
|
|
||||||
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
|
|
||||||
t->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
|
||||||
t->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
|
|
||||||
|
|
||||||
Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
t->bindWidget("play-btn", playBtn);
|
|
||||||
playBtn->addStyleClass("mobile-btn");
|
|
||||||
playBtn->clicked().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
int pos = _trackContainer->indexOf(t);
|
|
||||||
play(pos);
|
|
||||||
}));
|
|
||||||
|
|
||||||
Wt::WText *delBtn = new Wt::WText("<i class=\"fa fa-remove fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
t->bindWidget("del-btn", delBtn);
|
|
||||||
delBtn->addStyleClass("mobile-btn");
|
|
||||||
delBtn->clicked().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
int pos = _trackContainer->indexOf(t);
|
|
||||||
_trackContainer->removeWidget(t);
|
|
||||||
_trackIds.erase(_trackIds.begin() + pos);
|
|
||||||
}));
|
|
||||||
|
|
||||||
return trackPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::clear(void)
|
|
||||||
{
|
|
||||||
_trackIds.clear();
|
|
||||||
_trackContainer->clear();
|
|
||||||
_currentPos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::play(size_t pos)
|
|
||||||
{
|
|
||||||
if (_trackIds.empty() || pos >= _trackIds.size())
|
|
||||||
return;
|
|
||||||
|
|
||||||
_trackContainer->widget(_currentPos)->removeStyleClass("playqueue-playing");
|
|
||||||
|
|
||||||
_currentPos = pos;
|
|
||||||
_trackContainer->widget(_currentPos)->addStyleClass("playqueue-playing");
|
|
||||||
|
|
||||||
_sigTrackPlay.emit(_trackIds[pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::playNext(void)
|
|
||||||
{
|
|
||||||
if (_currentPos == (_trackIds.size() - 1) && _loop)
|
|
||||||
play(0);
|
|
||||||
else
|
|
||||||
play(_currentPos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::playPrevious(void)
|
|
||||||
{
|
|
||||||
if (_currentPos == 0)
|
|
||||||
{
|
|
||||||
if (_loop)
|
|
||||||
{
|
|
||||||
play(_trackIds.size() - 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
play(_currentPos - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::handlePlaybackComplete(void)
|
|
||||||
{
|
|
||||||
playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::setShuffle(bool value)
|
|
||||||
{
|
|
||||||
_shuffle = value;
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
_shuffleBtn->addStyleClass("mobile-btn-active");
|
|
||||||
else
|
|
||||||
_shuffleBtn->removeStyleClass("mobile-btn-active");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
PlayQueue::setLoop(bool value)
|
|
||||||
{
|
|
||||||
_loop = value;
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
_loopBtn->addStyleClass("mobile-btn-active");
|
|
||||||
else
|
|
||||||
_loopBtn->removeStyleClass("mobile-btn-active");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class PlayQueue : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PlayQueue(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
// Contents
|
|
||||||
std::size_t addArtist(Database::Artist::id_type id);
|
|
||||||
std::size_t addRelease(Database::Release::id_type id);
|
|
||||||
std::size_t addTrack(Database::Track::id_type id);
|
|
||||||
void clear(void);
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
void play(size_t pos); // Play the track at a given position
|
|
||||||
void playNext(void);
|
|
||||||
void playPrevious(void);
|
|
||||||
|
|
||||||
void setShuffle(bool enable); // true to enable shuffle
|
|
||||||
void setLoop(bool enable); // true to enable loop
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
// Emitted when a song has to be played
|
|
||||||
Wt::Signal<Database::Track::id_type>& playTrack() { return _sigTrackPlay; }
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
// Song has finished
|
|
||||||
void handlePlaybackComplete(void);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
bool _loop = false;
|
|
||||||
bool _shuffle = false;
|
|
||||||
std::size_t _currentPos = 0;
|
|
||||||
|
|
||||||
Wt::WText* _shuffleBtn = nullptr;
|
|
||||||
Wt::WText* _loopBtn = nullptr;
|
|
||||||
|
|
||||||
Wt::Signal<Database::Track::id_type> _sigTrackPlay;
|
|
||||||
|
|
||||||
std::vector<Database::Track::id_type> _trackIds;
|
|
||||||
Wt::WContainerWidget* _trackContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "SearchUtils.hpp"
|
|
||||||
|
|
||||||
#include "ArtistSearch.hpp"
|
|
||||||
#include "ReleaseSearch.hpp"
|
|
||||||
#include "TrackSearch.hpp"
|
|
||||||
|
|
||||||
#include "PreviewSearchView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
#define SEARCH_NB_ITEMS 4
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
PreviewSearchView::PreviewSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/search/preview";
|
|
||||||
|
|
||||||
ArtistSearch* artistSearch = new ArtistSearch("Artists", this);
|
|
||||||
ReleaseSearch* releaseSearch = new ReleaseSearch(this);
|
|
||||||
TrackSearch* trackSearch = new TrackSearch(events, this);
|
|
||||||
|
|
||||||
artistSearch->showMore().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
std::string path = wApp->internalPath();
|
|
||||||
path.replace(0, pathPrefix.length(), "/audio/search/artist");
|
|
||||||
|
|
||||||
wApp->setInternalPath(path, true);
|
|
||||||
}));
|
|
||||||
|
|
||||||
releaseSearch->showMore().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
std::string path = wApp->internalPath();
|
|
||||||
path.replace(0, pathPrefix.length(), "/audio/search/release");
|
|
||||||
|
|
||||||
wApp->setInternalPath(path, true);
|
|
||||||
}));
|
|
||||||
|
|
||||||
trackSearch->showMore().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
std::string path = wApp->internalPath();
|
|
||||||
path.replace(0, pathPrefix.length(), "/audio/search/track");
|
|
||||||
|
|
||||||
wApp->setInternalPath(path, true);
|
|
||||||
}));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
|
||||||
|
|
||||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
|
|
||||||
releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS, "Releases");
|
|
||||||
trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseSearch.hpp"
|
|
||||||
#include "ReleaseView.hpp"
|
|
||||||
#include "ArtistView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
ReleaseSearch::ReleaseSearch(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
|
||||||
t->setTemplateText(Wt::WString::tr("wa-release-search"));
|
|
||||||
|
|
||||||
Wt::WTemplate *titleTemplate = new Wt::WTemplate(this);
|
|
||||||
titleTemplate->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
|
||||||
|
|
||||||
_title = new Wt::WText();
|
|
||||||
titleTemplate->bindWidget("text", _title);
|
|
||||||
|
|
||||||
t->bindWidget("title", titleTemplate);
|
|
||||||
|
|
||||||
_contents = new Wt::WContainerWidget();
|
|
||||||
t->bindWidget("contents", _contents );
|
|
||||||
|
|
||||||
_showMore = new Wt::WTemplate();
|
|
||||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
|
||||||
_showMore->bindString("text", "Tap to show more results...");
|
|
||||||
_showMore->hide();
|
|
||||||
_showMore->clicked().connect(std::bind([=] {
|
|
||||||
_sigShowMore.emit();
|
|
||||||
}));
|
|
||||||
|
|
||||||
t->bindWidget("show-more", _showMore);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseSearch::clear()
|
|
||||||
{
|
|
||||||
_contents->clear();
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseSearch::search(Database::SearchFilter filter, size_t max, Wt::WString title)
|
|
||||||
{
|
|
||||||
_filter = filter;
|
|
||||||
_title->setText(title);
|
|
||||||
|
|
||||||
clear();
|
|
||||||
addResults(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseSearch::addResults(size_t nb)
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
bool moreResults;
|
|
||||||
std::vector<Release::pointer> releases = Release::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
|
||||||
|
|
||||||
for (Release::pointer release : releases)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
|
||||||
res->setTemplateText(Wt::WString::tr("wa-release-search-res"));
|
|
||||||
|
|
||||||
Wt::WAnchor *coverAnchor = new Wt::WAnchor(ReleaseView::getLink(release.id()));
|
|
||||||
Wt::WImage *cover = new Wt::WImage(coverAnchor);
|
|
||||||
cover->setStyleClass("center-block");
|
|
||||||
cover->setImageLink( SessionImageResource()->getReleaseUrl(release.id(), 512));
|
|
||||||
cover->setStyleClass("release_res_shadow release_img-responsive"); // TODO move?
|
|
||||||
|
|
||||||
res->bindWidget("cover", coverAnchor);
|
|
||||||
res->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText));
|
|
||||||
res->bindString("release_name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
|
||||||
|
|
||||||
bool variousArtists;
|
|
||||||
auto artists = Artist::getByFilter(DboSession(),
|
|
||||||
SearchFilter::ById(SearchFilter::Field::Release, release.id()), 0, 1, variousArtists);
|
|
||||||
if (!artists.empty())
|
|
||||||
{
|
|
||||||
if (variousArtists)
|
|
||||||
{
|
|
||||||
res->bindWidget("artist", new Wt::WText(Wt::WString::fromUTF8("Various Artists", Wt::PlainText)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Wt::WAnchor *artistAnchor = new Wt::WAnchor(ArtistView::getLink(artists.front().id()));
|
|
||||||
Wt::WText *artist = new Wt::WText(artistAnchor);
|
|
||||||
artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText));
|
|
||||||
res->bindWidget("artist", artistAnchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moreResults)
|
|
||||||
_showMore->show();
|
|
||||||
else
|
|
||||||
_showMore->hide();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
#include "database/SearchFilter.hpp"
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class ReleaseSearch : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
ReleaseSearch(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void search(Database::SearchFilter filter, std::size_t nb, Wt::WString title);
|
|
||||||
void addResults(std::size_t nb);
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigShowMore;
|
|
||||||
|
|
||||||
void clear(void);
|
|
||||||
|
|
||||||
Wt::WTemplate* _showMore;
|
|
||||||
Database::SearchFilter _filter;
|
|
||||||
Wt::WContainerWidget* _contents;
|
|
||||||
Wt::WText* _title;
|
|
||||||
std::size_t _count;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
|
|
||||||
#include "SearchUtils.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseSearch.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseSearchView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
#define SEARCH_NB_ITEMS 20
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
ReleaseSearchView::ReleaseSearchView(Wt::WContainerWidget* parent)
|
|
||||||
{
|
|
||||||
ReleaseSearch* artistSearch = new ReleaseSearch(this);
|
|
||||||
artistSearch->showMore().connect(std::bind([=] {
|
|
||||||
artistSearch->addResults(SEARCH_NB_ITEMS);
|
|
||||||
}));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/search/release";
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
|
||||||
|
|
||||||
artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS, "Releases");
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "ReleaseView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
ReleaseView::ReleaseView(PlayQueueEvents& events, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent),
|
|
||||||
_events(events)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* wrapper = new Wt::WTemplate(this);
|
|
||||||
wrapper->setTemplateText(Wt::WString::tr("wa-trackview-wrapper"));
|
|
||||||
|
|
||||||
Wt::WTemplate* title = new Wt::WTemplate(this);
|
|
||||||
wrapper->bindWidget("title", title);
|
|
||||||
|
|
||||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
|
||||||
title->bindString("text", "Releases", Wt::PlainText);
|
|
||||||
|
|
||||||
_releaseContainer = new Wt::WContainerWidget();
|
|
||||||
wrapper->bindWidget("release-container", _releaseContainer);
|
|
||||||
|
|
||||||
_showMore = new Wt::WTemplate();
|
|
||||||
wrapper->bindWidget("show-more", _showMore);
|
|
||||||
|
|
||||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
|
||||||
_showMore->bindString("text", "Tap to show more results...");
|
|
||||||
_showMore->hide();
|
|
||||||
_showMore->clicked().connect(std::bind([=] {
|
|
||||||
addResults(20);
|
|
||||||
}));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/release/";
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::string strId = path.substr(pathPrefix.length());
|
|
||||||
|
|
||||||
Release::id_type id;
|
|
||||||
if (readAs(strId, id))
|
|
||||||
{
|
|
||||||
clear();
|
|
||||||
search(SearchFilter::ById(SearchFilter::Field::Release, id), 20);
|
|
||||||
}
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseView::clear()
|
|
||||||
{
|
|
||||||
// Flush the release container
|
|
||||||
_releaseContainer->clear();
|
|
||||||
|
|
||||||
// Flush the current context
|
|
||||||
_currentTrackContainer = nullptr;
|
|
||||||
_nbTracks = 0;
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseView::search(SearchFilter filter, size_t nb)
|
|
||||||
{
|
|
||||||
_filter = filter;
|
|
||||||
|
|
||||||
clear();
|
|
||||||
addResults(nb);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Wt::WString
|
|
||||||
getArtistNameFromRelease(Release::pointer release)
|
|
||||||
{
|
|
||||||
auto artists = Artist::getByFilter(DboSession(),
|
|
||||||
SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
|
|
||||||
|
|
||||||
if (artists.size() > 1)
|
|
||||||
return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
|
|
||||||
else if (artists.size() == 1)
|
|
||||||
return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
|
|
||||||
else
|
|
||||||
return Wt::WString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
ReleaseView::addResults(size_t nb)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
bool moreResults;
|
|
||||||
std::vector<Track::pointer> tracks = Track::getByFilter(DboSession(), _filter, _nbTracks, nb, moreResults);
|
|
||||||
|
|
||||||
for (Track::pointer track : tracks)
|
|
||||||
{
|
|
||||||
// First check if we need to create a new track container
|
|
||||||
|
|
||||||
// New container if it is the first one or if the release has changed
|
|
||||||
if (!_currentTrackContainer
|
|
||||||
|| _currentReleaseId != track->getRelease().id())
|
|
||||||
{
|
|
||||||
Release::pointer release = track->getRelease();
|
|
||||||
|
|
||||||
Wt::WTemplate *releaseContainer = new Wt::WTemplate(_releaseContainer);
|
|
||||||
releaseContainer->setTemplateText(Wt::WString::tr("wa-trackview-release-container"));
|
|
||||||
_currentReleaseId = release.id();
|
|
||||||
|
|
||||||
Wt::WImage *cover = new Wt::WImage();
|
|
||||||
cover->setStyleClass ("center-block img-responsive"); // TODO move to CSS?
|
|
||||||
cover->setImageLink(Wt::WLink (SessionImageResource()->getReleaseUrl(release.id(), 512)));
|
|
||||||
|
|
||||||
releaseContainer->bindWidget("cover", cover);
|
|
||||||
releaseContainer->bindString("artist-name", getArtistNameFromRelease(release), Wt::PlainText);
|
|
||||||
releaseContainer->bindString("release-name", release->getName(), Wt::PlainText);
|
|
||||||
int year = release->getReleaseYear();
|
|
||||||
if (year > 0)
|
|
||||||
{
|
|
||||||
releaseContainer->setCondition("if-has-year", true);
|
|
||||||
releaseContainer->bindInt("year", year);
|
|
||||||
|
|
||||||
int originalYear = release->getReleaseYear(true);
|
|
||||||
if (originalYear > 0 && originalYear != year)
|
|
||||||
{
|
|
||||||
releaseContainer->setCondition("if-has-orig-year", true);
|
|
||||||
releaseContainer->bindInt("orig-year", originalYear);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentTrackContainer = new Wt::WContainerWidget();
|
|
||||||
releaseContainer->bindWidget("track-container", _currentTrackContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(_currentTrackContainer != nullptr);
|
|
||||||
|
|
||||||
Wt::WTemplate* trackRes = new Wt::WTemplate(_currentTrackContainer);
|
|
||||||
trackRes->setTemplateText(Wt::WString::tr("wa-trackview-track"));
|
|
||||||
|
|
||||||
if (track->getTrackNumber() > 0 && !track->getRelease()->isNone())
|
|
||||||
{
|
|
||||||
trackRes->setCondition("if-has-track-num", true);
|
|
||||||
trackRes->bindInt("track-num", track->getTrackNumber());
|
|
||||||
|
|
||||||
if (track->getDiscNumber() > 0 && track->getTotalDiscNumber() > 1)
|
|
||||||
{ trackRes->setCondition("if-has-disc-num", true);
|
|
||||||
trackRes->bindInt("disc-num", track->getDiscNumber());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
|
||||||
// TODO, display artist name for compilation releases?
|
|
||||||
|
|
||||||
Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
trackRes->bindWidget("play-btn", playBtn);
|
|
||||||
playBtn->setStyleClass("mobile-btn");
|
|
||||||
playBtn->clicked().connect(std::bind([=] {
|
|
||||||
_events.trackPlay.emit(track.id());
|
|
||||||
}));
|
|
||||||
|
|
||||||
Wt::WText *addBtn = new Wt::WText("<i class=\"fa fa-plus fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
trackRes->bindWidget("add-btn", addBtn);
|
|
||||||
addBtn->setStyleClass("mobile-btn");
|
|
||||||
addBtn->clicked().connect(std::bind([=] {
|
|
||||||
_events.trackAdd.emit(track.id());
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_nbTracks++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moreResults)
|
|
||||||
_showMore->show();
|
|
||||||
else
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WLink
|
|
||||||
ReleaseView::getLink(Database::Release::id_type id)
|
|
||||||
{
|
|
||||||
return Wt::WLink(Wt::WLink::InternalPath, "/audio/release/" + std::to_string(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "MobilePlayQueueEvents.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class ReleaseView : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
static Wt::WLink getLink(Database::Release::id_type id);
|
|
||||||
|
|
||||||
ReleaseView(PlayQueueEvents& events, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void search(Database::SearchFilter filter, size_t nb);
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
Wt::Signal<void>& moreTracksSelected() { return _sigMoreTracksSelected;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigMoreTracksSelected;
|
|
||||||
|
|
||||||
void clear(void);
|
|
||||||
void addResults(size_t nb);
|
|
||||||
|
|
||||||
PlayQueueEvents& _events;
|
|
||||||
Wt::WTemplate* _showMore;
|
|
||||||
|
|
||||||
Database::SearchFilter _filter;
|
|
||||||
std::size_t _nbTracks;
|
|
||||||
|
|
||||||
// Main container that holds the releases
|
|
||||||
Wt::WContainerWidget* _releaseContainer;
|
|
||||||
|
|
||||||
// Used to add more results on the fly
|
|
||||||
Database::Release::id_type _currentReleaseId;
|
|
||||||
Wt::WContainerWidget* _currentTrackContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
static inline std::vector<std::string> searchPathToSearchKeywords(std::string path)
|
|
||||||
{
|
|
||||||
if (!path.empty() && path[0] == '/')
|
|
||||||
path.erase(path.begin());
|
|
||||||
|
|
||||||
return splitString(path, " \t");
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "TrackSearch.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
TrackSearch::TrackSearch(PlayQueueEvents& events, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent),
|
|
||||||
_events(events)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* t = new Wt::WTemplate(this);
|
|
||||||
t->setTemplateText(Wt::WString::tr("wa-track-search"));
|
|
||||||
|
|
||||||
Wt::WTemplate* title = new Wt::WTemplate(this);
|
|
||||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
|
||||||
title->bindString("text", "Tracks", Wt::PlainText);
|
|
||||||
t->bindWidget("title", title);
|
|
||||||
|
|
||||||
_contents = new Wt::WContainerWidget();
|
|
||||||
t->bindWidget("contents", _contents);
|
|
||||||
|
|
||||||
_showMore = new Wt::WTemplate();
|
|
||||||
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
|
||||||
_showMore->bindString("text", "Tap to show more results...");
|
|
||||||
_showMore->hide();
|
|
||||||
_showMore->clicked().connect(std::bind([=] {
|
|
||||||
_sigShowMore.emit();
|
|
||||||
addResults(20);
|
|
||||||
}));
|
|
||||||
t->bindWidget("show-more", _showMore);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSearch::clear()
|
|
||||||
{
|
|
||||||
_contents->clear();
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSearch::search(SearchFilter filter, size_t nb)
|
|
||||||
{
|
|
||||||
_filter = filter;
|
|
||||||
|
|
||||||
clear();
|
|
||||||
addResults(nb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TrackSearch::addResults(size_t nb)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
bool moreResults;
|
|
||||||
std::vector<Track::pointer > tracks = Track::getByFilter(DboSession(), _filter, _contents->count(), nb, moreResults);
|
|
||||||
|
|
||||||
for (Track::pointer track : tracks)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* res = new Wt::WTemplate(_contents);
|
|
||||||
res->setTemplateText(Wt::WString::tr("wa-track-search-res"));
|
|
||||||
|
|
||||||
Wt::WImage *cover = new Wt::WImage();
|
|
||||||
res->bindWidget("cover", cover);
|
|
||||||
cover->setStyleClass ("center-block img-responsive");
|
|
||||||
cover->setImageLink(SessionImageResource()->getTrackUrl(track.id(), 64));
|
|
||||||
res->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
|
||||||
res->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
|
|
||||||
|
|
||||||
Wt::WText *playBtn = new Wt::WText("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
res->bindWidget("play-btn", playBtn);
|
|
||||||
playBtn->setStyleClass("mobile-btn");
|
|
||||||
playBtn->clicked().connect(std::bind([=] {
|
|
||||||
_events.trackPlay.emit(track.id());
|
|
||||||
}));
|
|
||||||
|
|
||||||
Wt::WText *addBtn = new Wt::WText("<i class=\"fa fa-plus fa-lg\"></i>", Wt::XHTMLText);
|
|
||||||
res->bindWidget("add-btn", addBtn);
|
|
||||||
addBtn->setStyleClass("mobile-btn");
|
|
||||||
addBtn->clicked().connect(std::bind([=] {
|
|
||||||
_events.trackAdd.emit(track.id());
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moreResults)
|
|
||||||
_showMore->show();
|
|
||||||
else
|
|
||||||
_showMore->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "database/SearchFilter.hpp"
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
#include "MobilePlayQueueEvents.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class TrackSearch : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
TrackSearch(PlayQueueEvents& events, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void addResults(size_t nb);
|
|
||||||
void search(Database::SearchFilter filter, size_t nb);
|
|
||||||
|
|
||||||
// Slots
|
|
||||||
Wt::Signal<void>& showMore() { return _sigShowMore;}
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigShowMore;
|
|
||||||
|
|
||||||
void clear(void);
|
|
||||||
|
|
||||||
PlayQueueEvents& _events;
|
|
||||||
Wt::WTemplate* _showMore;
|
|
||||||
Database::SearchFilter _filter;
|
|
||||||
Wt::WContainerWidget* _contents;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "SearchUtils.hpp"
|
|
||||||
|
|
||||||
#include "TrackSearch.hpp"
|
|
||||||
|
|
||||||
#include "TrackSearchView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
#define SEARCH_NB_ITEMS 20
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
TrackSearchView::TrackSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent)
|
|
||||||
{
|
|
||||||
TrackSearch* trackSearch = new TrackSearch(events, this);
|
|
||||||
trackSearch->showMore().connect(std::bind([=] {
|
|
||||||
trackSearch->addResults(SEARCH_NB_ITEMS);
|
|
||||||
}));
|
|
||||||
|
|
||||||
wApp->internalPathChanged().connect(std::bind([=] (std::string path)
|
|
||||||
{
|
|
||||||
const std::string pathPrefix = "/audio/search/track";
|
|
||||||
|
|
||||||
if (!wApp->internalPathMatches(pathPrefix))
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<std::string> keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length()));
|
|
||||||
|
|
||||||
trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
|
|
||||||
|
|
||||||
}, std::placeholders::_1));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Mobile
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WMenu>
|
|
||||||
#include <Wt/WStackedWidget>
|
|
||||||
#include <Wt/WTextArea>
|
|
||||||
#include <Wt/WHBoxLayout>
|
|
||||||
|
|
||||||
#include "SettingsAudioFormView.hpp"
|
|
||||||
#include "SettingsUserFormView.hpp"
|
|
||||||
#include "SettingsAccountFormView.hpp"
|
|
||||||
#include "SettingsDatabaseFormView.hpp"
|
|
||||||
#include "SettingsMediaDirectories.hpp"
|
|
||||||
#include "SettingsUsers.hpp"
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "database/Setting.hpp"
|
|
||||||
#include "database/updater/DatabaseUpdater.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "Settings.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
Settings::Settings(Wt::WContainerWidget* parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
Wt::WHBoxLayout* hLayout = new Wt::WHBoxLayout(this);
|
|
||||||
|
|
||||||
// Create a stack where the contents will be located.
|
|
||||||
Wt::WStackedWidget *contents = new Wt::WStackedWidget();
|
|
||||||
|
|
||||||
contents->setStyleClass("contents");
|
|
||||||
contents->setOverflow(WContainerWidget::OverflowHidden);
|
|
||||||
|
|
||||||
// TODO menu style on hover
|
|
||||||
Wt::WMenu *menu = new Wt::WMenu(contents, Wt::Vertical);
|
|
||||||
menu->setStyleClass("nav nav-pills nav-stacked submenu");
|
|
||||||
menu->setWidth(150);
|
|
||||||
menu->setInternalPathEnabled();
|
|
||||||
menu->setInternalBasePath("/settings/");
|
|
||||||
|
|
||||||
hLayout->addWidget(menu);
|
|
||||||
hLayout->addWidget(contents, 1);
|
|
||||||
|
|
||||||
std::string userId;
|
|
||||||
bool userIsAdmin;
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
userId = User::getId(CurrentUser());
|
|
||||||
userIsAdmin = CurrentUser()->isAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
menu->addItem("Audio", new AudioFormView())->setPathComponent("audio");
|
|
||||||
if (userIsAdmin)
|
|
||||||
{
|
|
||||||
MediaDirectories* mediaDirectories = new MediaDirectories();
|
|
||||||
mediaDirectories->changed().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Media directories have changed: requesting imediate scan";
|
|
||||||
|
|
||||||
// On directory change, request an immediate scan
|
|
||||||
std::lock_guard<std::mutex> lock(Updater::instance().getMutex());
|
|
||||||
|
|
||||||
Setting::setBool(DboSession(), "manual_scan_requested", true);
|
|
||||||
Updater::instance().restart();
|
|
||||||
}));
|
|
||||||
menu->addItem("Media Folders", mediaDirectories)->setPathComponent("mediadirectories");
|
|
||||||
|
|
||||||
DatabaseFormView* databaseFormView = new DatabaseFormView();
|
|
||||||
databaseFormView->changed().connect(std::bind([=]
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(Updater::instance().getMutex());
|
|
||||||
Updater::instance().restart();
|
|
||||||
}));
|
|
||||||
menu->addItem("Database", databaseFormView)->setPathComponent("database");
|
|
||||||
|
|
||||||
menu->addItem("Users", new Users())->setPathComponent("users");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
menu->addItem("Account", new AccountFormView(userId))->setPathComponent("account");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WCheckBox>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/Auth/Identity>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "common/Validators.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsAccountFormView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class AccountFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field NameField;
|
|
||||||
static const Field EmailField;
|
|
||||||
static const Field PasswordField;
|
|
||||||
static const Field PasswordConfirmField;
|
|
||||||
|
|
||||||
AccountFormModel(std::string userId, Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent),
|
|
||||||
_userId(userId)
|
|
||||||
{
|
|
||||||
addField(NameField);
|
|
||||||
addField(EmailField);
|
|
||||||
addField(PasswordField);
|
|
||||||
addField(PasswordConfirmField);
|
|
||||||
|
|
||||||
setValidator(NameField, createNameValidator());
|
|
||||||
setValidator(EmailField, createEmailValidator());
|
|
||||||
|
|
||||||
// populate the model with initial data
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadData()
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( _userId );
|
|
||||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
|
||||||
|
|
||||||
if (user && authUser.isValid())
|
|
||||||
{
|
|
||||||
setValue(NameField, authUser.identity(Wt::Auth::Identity::LoginName));
|
|
||||||
if (!authUser.email().empty())
|
|
||||||
setValue(EmailField, authUser.email());
|
|
||||||
else
|
|
||||||
setValue(EmailField, authUser.unverifiedEmail());
|
|
||||||
}
|
|
||||||
setValue(PasswordField, Wt::WString());
|
|
||||||
setValue(PasswordConfirmField, Wt::WString());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool saveData(Wt::WString& error)
|
|
||||||
{
|
|
||||||
// DBO transaction active here
|
|
||||||
try {
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Update user
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId(_userId);
|
|
||||||
Database::User::pointer user = DbHandler().getUser( authUser );
|
|
||||||
|
|
||||||
// user may have been deleted by someone else
|
|
||||||
if (!authUser.isValid()) {
|
|
||||||
error = Wt::WString("User identity does not exist");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if(!user)
|
|
||||||
{
|
|
||||||
error = Wt::WString("User not found");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account
|
|
||||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
|
||||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
|
||||||
|
|
||||||
// Password
|
|
||||||
if (!valueText(PasswordField).empty())
|
|
||||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
|
||||||
|
|
||||||
setValue(PasswordField, Wt::WString());
|
|
||||||
setValue(PasswordConfirmField, Wt::WString());
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& exception)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool validateField(Field field)
|
|
||||||
{
|
|
||||||
// DBO transaction active here
|
|
||||||
|
|
||||||
Wt::WString error;
|
|
||||||
|
|
||||||
if (field == NameField)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
// Must be unique since used as LoginIdentity
|
|
||||||
Wt::Auth::User user = DbHandler().getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, valueText(field));
|
|
||||||
if (user.isValid() && user.id() != _userId)
|
|
||||||
error = "Already exists";
|
|
||||||
else
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
else if (field == PasswordField)
|
|
||||||
{
|
|
||||||
// Password is mandatory if we create the user
|
|
||||||
if (!valueText(PasswordField).empty())
|
|
||||||
{
|
|
||||||
// Evaluate the strength of the password
|
|
||||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
|
||||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
|
||||||
valueText(NameField),
|
|
||||||
valueText(EmailField).toUTF8());
|
|
||||||
|
|
||||||
if (!res.isValid())
|
|
||||||
error = res.message();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
else if (field == PasswordConfirmField)
|
|
||||||
{
|
|
||||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
|
||||||
{
|
|
||||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
|
||||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Apply validators
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
|
||||||
|
|
||||||
return validation(field).state() == Wt::WValidator::Valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
std::string _userId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field AccountFormModel::NameField = "name";
|
|
||||||
const Wt::WFormModel::Field AccountFormModel::EmailField = "email";
|
|
||||||
const Wt::WFormModel::Field AccountFormModel::PasswordField = "password";
|
|
||||||
const Wt::WFormModel::Field AccountFormModel::PasswordConfirmField = "password-confirm";
|
|
||||||
|
|
||||||
AccountFormView::AccountFormView(std::string userId, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_model = new AccountFormModel(userId, this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-account"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
_applyInfo = new Wt::WText();
|
|
||||||
_applyInfo->setInline(false);
|
|
||||||
_applyInfo->hide();
|
|
||||||
bindWidget("apply-info", _applyInfo);
|
|
||||||
|
|
||||||
// Name
|
|
||||||
Wt::WLineEdit* accountEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(AccountFormModel::NameField, accountEdit);
|
|
||||||
accountEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Email
|
|
||||||
Wt::WLineEdit* emailEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(AccountFormModel::EmailField, emailEdit);
|
|
||||||
emailEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Password
|
|
||||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(AccountFormModel::PasswordField, passwordEdit );
|
|
||||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
passwordEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Password confirmation
|
|
||||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(AccountFormModel::PasswordConfirmField, passwordConfirmEdit);
|
|
||||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
passwordConfirmEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
bindString("title", "Account settings");
|
|
||||||
|
|
||||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
|
||||||
saveButton->setStyleClass("btn-primary");
|
|
||||||
bindWidget("save-button", saveButton);
|
|
||||||
saveButton->clicked().connect(this, &AccountFormView::processSave);
|
|
||||||
|
|
||||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Discard");
|
|
||||||
bindWidget("cancel-button", cancelButton);
|
|
||||||
cancelButton->clicked().connect(this, &AccountFormView::processCancel);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AccountFormView::processCancel()
|
|
||||||
{
|
|
||||||
_applyInfo->show();
|
|
||||||
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-info");
|
|
||||||
_model->loadData();
|
|
||||||
|
|
||||||
_model->validate();
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AccountFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
_applyInfo->show();
|
|
||||||
if (_model->validate())
|
|
||||||
{
|
|
||||||
Wt::WString error;
|
|
||||||
// commit model into DB
|
|
||||||
if (_model->saveData(error) )
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-success");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_ACCOUNT_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_ACCOUNT_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class AccountFormModel;
|
|
||||||
|
|
||||||
class AccountFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
AccountFormView(std::string userId, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void processCancel(); // reload from DB
|
|
||||||
void processSave(); // commit into DB
|
|
||||||
|
|
||||||
AccountFormModel* _model;
|
|
||||||
Wt::WText* _applyInfo;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WStringListModel>
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "common/Validators.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsAudioFormView.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::string encodingToString(Database::AudioEncoding encoding)
|
|
||||||
{
|
|
||||||
|
|
||||||
switch (encoding)
|
|
||||||
{
|
|
||||||
case Database::AudioEncoding::AUTO: return "Automatic";
|
|
||||||
case Database::AudioEncoding::MP3: return "MP3";
|
|
||||||
case Database::AudioEncoding::OGA: return "OGG";
|
|
||||||
case Database::AudioEncoding::WEBMA: return "WebM";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class AudioFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field BitrateField;
|
|
||||||
static const Field EncodingField;
|
|
||||||
|
|
||||||
AudioFormModel(Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent)
|
|
||||||
{
|
|
||||||
initializeModels();
|
|
||||||
|
|
||||||
addField(BitrateField);
|
|
||||||
addField(EncodingField);
|
|
||||||
|
|
||||||
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
|
||||||
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
|
||||||
|
|
||||||
// populate the model with initial data
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WAbstractItemModel *bitrateModel() { return _bitrateModel; }
|
|
||||||
Wt::WAbstractItemModel *encodingModel() { return _encodingModel; }
|
|
||||||
|
|
||||||
void loadData()
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
setValue(BitrateField, std::min(CurrentUser()->getMaxAudioBitrate(), CurrentUser()->getAudioBitrate()) / 1000); // in kps
|
|
||||||
int encodingRow = getAudioEncodingRow( CurrentUser()->getAudioEncoding());
|
|
||||||
setValue(EncodingField, getAudioEncodingString(encodingRow));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool saveData(Wt::WString& error)
|
|
||||||
{
|
|
||||||
// DBO transaction active here
|
|
||||||
try {
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// user may have been deleted by someone else
|
|
||||||
CurrentUser().modify()->setAudioBitrate( Wt::asNumber(value(BitrateField)) * 1000); // in kbps
|
|
||||||
|
|
||||||
int encodingRow = getAudioEncodingRow( boost::any_cast<Wt::WString>(value((EncodingField))));
|
|
||||||
CurrentUser().modify()->setAudioEncoding( getAudioEncodingValue(encodingRow));
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& exception)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getAudioEncodingRow( Database::AudioEncoding encoding)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _encodingModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (getAudioEncodingValue(i) == encoding)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getAudioEncodingRow( Wt::WString encoding)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _encodingModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (getAudioEncodingString(i) == encoding)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::AudioEncoding getAudioEncodingValue(int row)
|
|
||||||
{
|
|
||||||
return boost::any_cast<Database::AudioEncoding>
|
|
||||||
(_encodingModel->data(_encodingModel->index(row, 0), Wt::UserRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WString getAudioEncodingString(int row)
|
|
||||||
{
|
|
||||||
return boost::any_cast<Wt::WString>
|
|
||||||
(_encodingModel->data(_encodingModel->index(row, 0), Wt::DisplayRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
void initializeModels()
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
_bitrateModel = new Wt::WStringListModel(this);
|
|
||||||
BOOST_FOREACH(std::size_t bitrate, Database::User::audioBitrates)
|
|
||||||
{
|
|
||||||
if (bitrate <= CurrentUser()->getMaxAudioBitrate())
|
|
||||||
_bitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps
|
|
||||||
}
|
|
||||||
|
|
||||||
_encodingModel = new Wt::WStringListModel(this);
|
|
||||||
int row = 0;
|
|
||||||
BOOST_FOREACH(Database::AudioEncoding encoding, Database::User::audioEncodings)
|
|
||||||
{
|
|
||||||
_encodingModel->addString(encodingToString(encoding));
|
|
||||||
_encodingModel->setData(row, 0, encoding, Wt::UserRole);
|
|
||||||
row++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string _userId;
|
|
||||||
Wt::WStringListModel* _bitrateModel;
|
|
||||||
Wt::WStringListModel* _encodingModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field AudioFormModel::BitrateField = "bitrate";
|
|
||||||
const Wt::WFormModel::Field AudioFormModel::EncodingField = "encoding";
|
|
||||||
|
|
||||||
AudioFormView::AudioFormView(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_model = new AudioFormModel(this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-audio"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
_applyInfo = new Wt::WText();
|
|
||||||
_applyInfo->setInline(false);
|
|
||||||
_applyInfo->hide();
|
|
||||||
bindWidget("apply-info", _applyInfo);
|
|
||||||
|
|
||||||
// Bitrate
|
|
||||||
Wt::WComboBox *bitrateCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(AudioFormModel::BitrateField, bitrateCB);
|
|
||||||
bitrateCB->setStyleClass("span2");
|
|
||||||
bitrateCB->setModel(_model->bitrateModel());
|
|
||||||
bitrateCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Encoding
|
|
||||||
Wt::WComboBox *encodingCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(AudioFormModel::EncodingField, encodingCB);
|
|
||||||
encodingCB->setModel(_model->encodingModel());
|
|
||||||
encodingCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
bindString("title", "Audio settings");
|
|
||||||
|
|
||||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
|
||||||
saveButton->setStyleClass("btn-primary");
|
|
||||||
bindWidget("save-button", saveButton);
|
|
||||||
saveButton->clicked().connect(this, &AudioFormView::processSave);
|
|
||||||
|
|
||||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Discard");
|
|
||||||
bindWidget("cancel-button", cancelButton);
|
|
||||||
cancelButton->clicked().connect(this, &AudioFormView::processCancel);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioFormView::processCancel()
|
|
||||||
{
|
|
||||||
_applyInfo->show();
|
|
||||||
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-info");
|
|
||||||
_model->loadData();
|
|
||||||
|
|
||||||
_model->validate();
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
_applyInfo->show();
|
|
||||||
if (_model->validate())
|
|
||||||
{
|
|
||||||
Wt::WString error;
|
|
||||||
// commit model into DB
|
|
||||||
if (_model->saveData(error) )
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-success");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_AUDIO_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_AUDIO_ACCOUNT_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WText>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class AudioFormModel;
|
|
||||||
|
|
||||||
class AudioFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
AudioFormView(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void processCancel(); // reload from DB
|
|
||||||
void processSave(); // commit into DB
|
|
||||||
|
|
||||||
AudioFormModel* _model;
|
|
||||||
Wt::WText* _applyInfo;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WString>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WCheckBox>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WMessageBox>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WSpinBox>
|
|
||||||
#include <Wt/WRegExpValidator>
|
|
||||||
#include <Wt/WIntValidator>
|
|
||||||
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WStringListModel>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "database/Setting.hpp"
|
|
||||||
|
|
||||||
#include "common/DirectoryValidator.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsDatabaseFormView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
class DatabaseFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field UpdatePeriodField;
|
|
||||||
static const Field UpdateStartTimeField;
|
|
||||||
static const Field AudioFileExtensionsField;
|
|
||||||
static const Field VideoFileExtensionsField;
|
|
||||||
static const Field TagsHighLevelAcousticBrainz;
|
|
||||||
static const Field TagsHighLevelAcousticBrainzMinProbability;
|
|
||||||
static const Field TagsSimilarityAcousticBrainz;
|
|
||||||
|
|
||||||
DatabaseFormModel(Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent)
|
|
||||||
{
|
|
||||||
initializeModels();
|
|
||||||
|
|
||||||
addField(UpdatePeriodField);
|
|
||||||
addField(UpdateStartTimeField);
|
|
||||||
addField(AudioFileExtensionsField);
|
|
||||||
addField(VideoFileExtensionsField);
|
|
||||||
addField(TagsHighLevelAcousticBrainz);
|
|
||||||
addField(TagsHighLevelAcousticBrainzMinProbability);
|
|
||||||
addField(TagsSimilarityAcousticBrainz);
|
|
||||||
|
|
||||||
setValidator(UpdatePeriodField, createUpdatePeriodValidator());
|
|
||||||
setValidator(UpdateStartTimeField, createStartTimeValidator());
|
|
||||||
setValidator(AudioFileExtensionsField, createFileExtensionValidator());
|
|
||||||
setValidator(VideoFileExtensionsField, createFileExtensionValidator());
|
|
||||||
setValidator(TagsHighLevelAcousticBrainzMinProbability, createMinProbabilityValidator());
|
|
||||||
|
|
||||||
// populate the model with initial data
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WAbstractItemModel *updatePeriodModel() { return _updatePeriodModel; }
|
|
||||||
Wt::WAbstractItemModel *updateStartTimeModel() { return _updateStartTimeModel; }
|
|
||||||
|
|
||||||
void loadData()
|
|
||||||
{
|
|
||||||
int periodRow = getUpdatePeriodModelRow( Setting::getString(DboSession(), "update_period"));
|
|
||||||
if (periodRow != -1)
|
|
||||||
setValue(UpdatePeriodField, updatePeriodDisplay(periodRow));
|
|
||||||
|
|
||||||
int startTimeRow = getUpdateStartTimeModelRow( Setting::getDuration(DboSession(), "update_start_time") );
|
|
||||||
if (startTimeRow != -1)
|
|
||||||
setValue(UpdateStartTimeField, updateStartTime( startTimeRow ) );
|
|
||||||
|
|
||||||
setValue(AudioFileExtensionsField, Setting::getString(DboSession(), "audio_file_extensions"));
|
|
||||||
setValue(VideoFileExtensionsField, Setting::getString(DboSession(), "video_file_extensions"));
|
|
||||||
|
|
||||||
setValue(TagsHighLevelAcousticBrainz, Setting::getBool(DboSession(), "tags_highlevel_acousticbrainz"));
|
|
||||||
setValue(TagsHighLevelAcousticBrainzMinProbability, Setting::getInt(DboSession(), "tags_highlevel_acousticbrainz_min_probability"));
|
|
||||||
setValue(TagsSimilarityAcousticBrainz, Setting::getBool(DboSession(), "tags_similarity_acousticbrainz"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveData()
|
|
||||||
{
|
|
||||||
int periodRow = getUpdatePeriodModelRow( boost::any_cast<Wt::WString>(value(UpdatePeriodField)));
|
|
||||||
assert(periodRow != -1);
|
|
||||||
Setting::setString(DboSession(), "update_period", updatePeriodSetting( periodRow ) );
|
|
||||||
|
|
||||||
int startTimeRow = getUpdateStartTimeModelRow( boost::any_cast<Wt::WString>(value(UpdateStartTimeField)));
|
|
||||||
assert(startTimeRow != -1);
|
|
||||||
Setting::setDuration(DboSession(), "update_start_time", updateStartTimeDuration( startTimeRow ) );
|
|
||||||
|
|
||||||
Setting::setString(DboSession(), "audio_file_extensions", boost::any_cast<Wt::WString>(value(AudioFileExtensionsField)).toUTF8());
|
|
||||||
Setting::setString(DboSession(), "video_file_extensions", boost::any_cast<Wt::WString>(value(VideoFileExtensionsField)).toUTF8());
|
|
||||||
|
|
||||||
Setting::setBool(DboSession(), "tags_highlevel_acousticbrainz", boost::any_cast<bool>(value(TagsHighLevelAcousticBrainz)));
|
|
||||||
Setting::setInt(DboSession(), "tags_highlevel_acousticbrainz_min_probability", std::stoi(boost::any_cast<Wt::WString>(value(TagsHighLevelAcousticBrainzMinProbability)).toUTF8()));
|
|
||||||
Setting::setBool(DboSession(), "tags_similarity_acousticbrainz", boost::any_cast<bool>(value(TagsSimilarityAcousticBrainz)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setImmediateScan()
|
|
||||||
{
|
|
||||||
Setting::setBool(DboSession(), "manual_scan_requested", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int getUpdatePeriodModelRow(Wt::WString value)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (updatePeriodDisplay(i) == value)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getUpdatePeriodModelRow(std::string value)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (updatePeriodSetting(i) == value)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string updatePeriodSetting(int row) {
|
|
||||||
return boost::any_cast<std::string>
|
|
||||||
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::UserRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WString updatePeriodDisplay(int row) {
|
|
||||||
return boost::any_cast<Wt::WString>
|
|
||||||
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::DisplayRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int getUpdateStartTimeModelRow(Wt::WString value)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (updateStartTime(i) == value)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getUpdateStartTimeModelRow(boost::posix_time::time_duration duration)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
|
|
||||||
{
|
|
||||||
if (updateStartTimeDuration(i) == duration)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
boost::posix_time::time_duration updateStartTimeDuration(int row) {
|
|
||||||
return boost::any_cast<boost::posix_time::time_duration>
|
|
||||||
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::UserRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WString updateStartTime(int row) {
|
|
||||||
return boost::any_cast<Wt::WString>
|
|
||||||
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::DisplayRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void initializeModels() {
|
|
||||||
|
|
||||||
_updatePeriodModel = new Wt::WStringListModel(this);
|
|
||||||
|
|
||||||
_updatePeriodModel->addString("Never");
|
|
||||||
_updatePeriodModel->setData(0, 0, std::string("never"), Wt::UserRole);
|
|
||||||
|
|
||||||
_updatePeriodModel->addString("Daily");
|
|
||||||
_updatePeriodModel->setData(1, 0, std::string("daily"), Wt::UserRole);
|
|
||||||
|
|
||||||
_updatePeriodModel->addString("Weekly");
|
|
||||||
_updatePeriodModel->setData(2, 0, std::string("weekly"), Wt::UserRole);
|
|
||||||
|
|
||||||
_updatePeriodModel->addString("Monthly");
|
|
||||||
_updatePeriodModel->setData(3, 0, std::string("monthly"), Wt::UserRole);
|
|
||||||
|
|
||||||
_updateStartTimeModel = new Wt::WStringListModel(this);
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < 24; ++i)
|
|
||||||
{
|
|
||||||
boost::posix_time::time_duration dur = boost::posix_time::hours(i);
|
|
||||||
|
|
||||||
boost::posix_time::time_facet* facet = new boost::posix_time::time_facet("%H:%M");
|
|
||||||
facet->time_duration_format("%H:%M");
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss.imbue(std::locale(oss.getloc(), facet));
|
|
||||||
|
|
||||||
oss << dur;
|
|
||||||
|
|
||||||
_updateStartTimeModel->addString( oss.str() );
|
|
||||||
_updateStartTimeModel->setData(i, 0, dur, Wt::UserRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WValidator *createUpdatePeriodValidator()
|
|
||||||
{
|
|
||||||
Wt::WValidator* v = new Wt::WValidator();
|
|
||||||
v->setMandatory(true);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WValidator *createStartTimeValidator()
|
|
||||||
{
|
|
||||||
Wt::WValidator* v = new Wt::WValidator();
|
|
||||||
v->setMandatory(true);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WValidator *createFileExtensionValidator()
|
|
||||||
{
|
|
||||||
Wt::WRegExpValidator *v = new Wt::WRegExpValidator("(?:\\.\\w+(?:\\s*))+");
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WValidator *createMinProbabilityValidator()
|
|
||||||
{
|
|
||||||
Wt::WIntValidator *v = new Wt::WIntValidator(50, 100);
|
|
||||||
v->setMandatory(true);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WStringListModel* _updatePeriodModel;
|
|
||||||
Wt::WStringListModel* _updateStartTimeModel;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::UpdatePeriodField = "update-period";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::UpdateStartTimeField = "update-start-time";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::AudioFileExtensionsField = "audio-file-extensions";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::VideoFileExtensionsField = "video-file-extensions";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::TagsHighLevelAcousticBrainz = "tags-highlevel-acousticbrainz";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability = "tags-highlevel-acousticbrainz-min-probability";
|
|
||||||
const Wt::WFormModel::Field DatabaseFormModel::TagsSimilarityAcousticBrainz = "tags-similarity-acousticbrainz";
|
|
||||||
|
|
||||||
|
|
||||||
DatabaseFormView::DatabaseFormView(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
_model = new DatabaseFormModel(this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-database"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
_applyInfo = new Wt::WText();
|
|
||||||
_applyInfo->setInline(false);
|
|
||||||
_applyInfo->hide();
|
|
||||||
bindWidget("apply-info", _applyInfo);
|
|
||||||
|
|
||||||
// Update Period
|
|
||||||
Wt::WComboBox *updatePeriodCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(DatabaseFormModel::UpdatePeriodField, updatePeriodCB);
|
|
||||||
updatePeriodCB->setModel(_model->updatePeriodModel());
|
|
||||||
updatePeriodCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Update Start Time
|
|
||||||
Wt::WComboBox *updateStartTimeCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(DatabaseFormModel::UpdateStartTimeField, updateStartTimeCB);
|
|
||||||
updateStartTimeCB->setModel(_model->updateStartTimeModel());
|
|
||||||
updateStartTimeCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Audio file extensions
|
|
||||||
Wt::WLineEdit *audioFileExtensionsEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(DatabaseFormModel::AudioFileExtensionsField, audioFileExtensionsEdit);
|
|
||||||
audioFileExtensionsEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Video file extensions
|
|
||||||
Wt::WLineEdit *videoFileExtensionsEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(DatabaseFormModel::VideoFileExtensionsField, videoFileExtensionsEdit);
|
|
||||||
videoFileExtensionsEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Tags from AB high level
|
|
||||||
Wt::WCheckBox* highLevel = new Wt::WCheckBox();
|
|
||||||
setFormWidget(DatabaseFormModel::TagsHighLevelAcousticBrainz, highLevel);
|
|
||||||
|
|
||||||
setFormWidget(DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability, new Wt::WSpinBox());
|
|
||||||
|
|
||||||
highLevel->changed().connect(std::bind([=] {
|
|
||||||
_model->setReadOnly(DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability,
|
|
||||||
!(highLevel->checkState() == Wt::Checked));
|
|
||||||
updateModel(_model);
|
|
||||||
updateViewField(_model, DatabaseFormModel::TagsHighLevelAcousticBrainzMinProbability);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Tags from AB similarity
|
|
||||||
setFormWidget(DatabaseFormModel::TagsSimilarityAcousticBrainz, new Wt::WCheckBox());
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
bindString("scan-title", "Scan settings");
|
|
||||||
bindString("tags-title", "Tag settings");
|
|
||||||
|
|
||||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Apply");
|
|
||||||
bindWidget("apply-button", saveButton);
|
|
||||||
saveButton->setStyleClass("btn-primary");
|
|
||||||
|
|
||||||
Wt::WPushButton *discardButton = new Wt::WPushButton("Discard");
|
|
||||||
bindWidget("discard-button", discardButton);
|
|
||||||
|
|
||||||
saveButton->clicked().connect(this, &DatabaseFormView::processSave);
|
|
||||||
discardButton->clicked().connect(this, &DatabaseFormView::processDiscard);
|
|
||||||
|
|
||||||
Wt::WPushButton *immediateScanButton = new Wt::WPushButton("Immediate scan");
|
|
||||||
immediateScanButton->setStyleClass("btn-warning");
|
|
||||||
bindWidget("immediate-scan-button", immediateScanButton);
|
|
||||||
immediateScanButton->clicked().connect(this, &DatabaseFormView::processImmediateScan);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
DatabaseFormView::processImmediateScan()
|
|
||||||
{
|
|
||||||
|
|
||||||
_model->setImmediateScan();
|
|
||||||
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Media folder scan has been started!" ) );
|
|
||||||
_applyInfo->setStyleClass("alert alert-warning");
|
|
||||||
_applyInfo->show();
|
|
||||||
|
|
||||||
_sigChanged.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
DatabaseFormView::processDiscard()
|
|
||||||
{
|
|
||||||
_applyInfo->show();
|
|
||||||
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Parameters reverted!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-info");
|
|
||||||
|
|
||||||
_model->loadData();
|
|
||||||
|
|
||||||
_model->validate();
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
DatabaseFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
_applyInfo->show();
|
|
||||||
|
|
||||||
if (_model->validate()) {
|
|
||||||
// Make the model to commit data into DB
|
|
||||||
_model->saveData();
|
|
||||||
|
|
||||||
_sigChanged.emit();
|
|
||||||
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-success");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
// Udate the view: Delete any validation message in the view, etc.
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_DB_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_DB_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class DatabaseFormModel;
|
|
||||||
|
|
||||||
class DatabaseFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
DatabaseFormView(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
Wt::Signal<void>& changed() { return _sigChanged; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigChanged;
|
|
||||||
|
|
||||||
void processSave();
|
|
||||||
void processDiscard();
|
|
||||||
void processImmediateScan();
|
|
||||||
|
|
||||||
Wt::WText *_applyInfo;
|
|
||||||
DatabaseFormModel *_model;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/Auth/Identity>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "common/Validators.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsFirstConnectionFormView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class FirstConnectionFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field NameField;
|
|
||||||
static const Field EmailField;
|
|
||||||
static const Field PasswordField;
|
|
||||||
static const Field PasswordConfirmField;
|
|
||||||
|
|
||||||
FirstConnectionFormModel(Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent)
|
|
||||||
{
|
|
||||||
addField(NameField);
|
|
||||||
addField(EmailField);
|
|
||||||
addField(PasswordField);
|
|
||||||
addField(PasswordConfirmField);
|
|
||||||
|
|
||||||
setValidator(NameField, createNameValidator());
|
|
||||||
setValidator(EmailField, createEmailValidator());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool saveData(Wt::WString& error)
|
|
||||||
{
|
|
||||||
// DBO transaction active here
|
|
||||||
try {
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Check if a user already exist
|
|
||||||
// If it's the case, just do nothing
|
|
||||||
if (!Database::User::getAll(DboSession()).empty())
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Admin user already created";
|
|
||||||
error = Wt::WString("Admin user already created!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().registerNew();
|
|
||||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
|
||||||
|
|
||||||
// Account
|
|
||||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
|
||||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
|
||||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
|
||||||
|
|
||||||
user.modify()->setAdmin( true );
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& exception)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
|
||||||
error = Wt::WString(exception.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool validateField(Field field)
|
|
||||||
{
|
|
||||||
// DBO transaction active here
|
|
||||||
|
|
||||||
Wt::WString error;
|
|
||||||
|
|
||||||
if (field == PasswordField)
|
|
||||||
{
|
|
||||||
// Password is mandatory if we create the user
|
|
||||||
if (!valueText(PasswordField).empty())
|
|
||||||
{
|
|
||||||
// Evaluate the strength of the password
|
|
||||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
|
||||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
|
||||||
valueText(NameField),
|
|
||||||
valueText(EmailField).toUTF8());
|
|
||||||
|
|
||||||
if (!res.isValid())
|
|
||||||
error = res.message();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
else if (field == PasswordConfirmField)
|
|
||||||
{
|
|
||||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
|
||||||
{
|
|
||||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
|
||||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Apply validators
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
|
||||||
|
|
||||||
return validation(field).state() == Wt::WValidator::Valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field FirstConnectionFormModel::NameField = "name";
|
|
||||||
const Wt::WFormModel::Field FirstConnectionFormModel::EmailField = "email";
|
|
||||||
const Wt::WFormModel::Field FirstConnectionFormModel::PasswordField = "password";
|
|
||||||
const Wt::WFormModel::Field FirstConnectionFormModel::PasswordConfirmField = "password-confirm";
|
|
||||||
|
|
||||||
FirstConnectionFormView::FirstConnectionFormView(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_model = new FirstConnectionFormModel(this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-first-connection"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
_applyInfo = new Wt::WText();
|
|
||||||
_applyInfo->setInline(false);
|
|
||||||
_applyInfo->hide();
|
|
||||||
bindWidget("apply-info", _applyInfo);
|
|
||||||
|
|
||||||
// Name
|
|
||||||
Wt::WLineEdit* accountEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(FirstConnectionFormModel::NameField, accountEdit);
|
|
||||||
accountEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Email
|
|
||||||
Wt::WLineEdit* emailEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(FirstConnectionFormModel::EmailField, emailEdit);
|
|
||||||
emailEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Password
|
|
||||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(FirstConnectionFormModel::PasswordField, passwordEdit );
|
|
||||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
passwordEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Password confirmation
|
|
||||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(FirstConnectionFormModel::PasswordConfirmField, passwordConfirmEdit);
|
|
||||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
passwordConfirmEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
bindString("title", "Create Admin account");
|
|
||||||
|
|
||||||
_saveButton = new Wt::WPushButton("Create");
|
|
||||||
_saveButton->setStyleClass("btn-primary");
|
|
||||||
bindWidget("save-button", _saveButton);
|
|
||||||
_saveButton->clicked().connect(this, &FirstConnectionFormView::processSave);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FirstConnectionFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
_applyInfo->show();
|
|
||||||
if (_model->validate())
|
|
||||||
{
|
|
||||||
Wt::WString error;
|
|
||||||
// commit model into DB
|
|
||||||
if (_model->saveData(error) )
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("New parameters successfully applied! Please refresh this page in order to login"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-success");
|
|
||||||
_saveButton->hide();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_FIRST_CONNECTION_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_FIRST_CONNECTION_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class FirstConnectionFormModel;
|
|
||||||
|
|
||||||
class FirstConnectionFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
FirstConnectionFormView(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void processSave();
|
|
||||||
|
|
||||||
Wt::WPushButton* _saveButton;
|
|
||||||
FirstConnectionFormModel* _model;
|
|
||||||
Wt::WText* _applyInfo;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WGroupBox>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WMessageBox>
|
|
||||||
|
|
||||||
#include "database/MediaDirectory.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsMediaDirectoryFormView.hpp"
|
|
||||||
|
|
||||||
#include "SettingsMediaDirectories.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
MediaDirectories::MediaDirectories(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
// Stack two widgets:
|
|
||||||
_stack = new Wt::WStackedWidget(this);
|
|
||||||
|
|
||||||
// 1/ the media directory table view
|
|
||||||
{
|
|
||||||
Wt::WGroupBox *container = new Wt::WGroupBox("Media Folders", _stack);
|
|
||||||
|
|
||||||
_table = new Wt::WTable(container);
|
|
||||||
|
|
||||||
_table->addStyleClass("table form-inline");
|
|
||||||
|
|
||||||
_table->toggleStyleClass("table-hover", true);
|
|
||||||
_table->toggleStyleClass("table-striped", true);
|
|
||||||
|
|
||||||
_table->setHeaderCount(1);
|
|
||||||
|
|
||||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("#"));
|
|
||||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Path"));
|
|
||||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("Type"));
|
|
||||||
|
|
||||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add Folder");
|
|
||||||
addBtn->setStyleClass("btn-success");
|
|
||||||
container->addWidget( addBtn );
|
|
||||||
addBtn->clicked().connect(boost::bind(&MediaDirectories::handleCreateMediaDirectory, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectories::refresh(void)
|
|
||||||
{
|
|
||||||
|
|
||||||
assert(_table->rowCount() > 0);
|
|
||||||
for (int i = _table->rowCount() - 1; i > 0; --i)
|
|
||||||
_table->deleteRow(i);
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
std::vector<Database::MediaDirectory::pointer> mediaDirectories = Database::MediaDirectory::getAll(DboSession());
|
|
||||||
|
|
||||||
std::size_t id = 1;
|
|
||||||
BOOST_FOREACH(Database::MediaDirectory::pointer mediaDirectory, mediaDirectories)
|
|
||||||
{
|
|
||||||
_table->elementAt(id, 0)->addWidget(new Wt::WText( Wt::WString::fromUTF8("{1}").arg(id)));
|
|
||||||
_table->elementAt(id, 1)->addWidget(new Wt::WText( Wt::WString::fromUTF8(mediaDirectory->getPath().string()) ));
|
|
||||||
|
|
||||||
Wt::WString dirType;
|
|
||||||
switch(mediaDirectory->getType())
|
|
||||||
{
|
|
||||||
case Database::MediaDirectory::Video: dirType = "Video"; break;
|
|
||||||
case Database::MediaDirectory::Audio: dirType = "Audio"; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_table->elementAt(id, 2)->addWidget( new Wt::WText( dirType ));
|
|
||||||
|
|
||||||
Wt::WPushButton* delBtn = new Wt::WPushButton("Delete");
|
|
||||||
delBtn->setStyleClass("btn-danger");
|
|
||||||
_table->elementAt(id, 3)->addWidget(delBtn);
|
|
||||||
delBtn->clicked().connect(boost::bind( &MediaDirectories::handleDelMediaDirectory, this, mediaDirectory->getPath(), mediaDirectory->getType() ) );
|
|
||||||
|
|
||||||
++id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectories::handleDelMediaDirectory(boost::filesystem::path p, Database::MediaDirectory::Type type)
|
|
||||||
{
|
|
||||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
|
||||||
("Delete Folder",
|
|
||||||
Wt::WString( "Deleting folder '{1}'?").arg(p.string()),
|
|
||||||
Wt::Question, Wt::Yes | Wt::No);
|
|
||||||
|
|
||||||
messageBox->setModal(true);
|
|
||||||
|
|
||||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
|
||||||
if (messageBox->buttonResult() == Wt::Yes)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Delete the media diretory
|
|
||||||
Database::MediaDirectory::pointer mediaDirectory = Database::MediaDirectory::get(DboSession(), p, type);
|
|
||||||
if (mediaDirectory)
|
|
||||||
mediaDirectory.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
// Emit something changed in the settings
|
|
||||||
_sigChanged.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete messageBox;
|
|
||||||
}));
|
|
||||||
|
|
||||||
messageBox->show();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectories::handleCreateMediaDirectory(void)
|
|
||||||
{
|
|
||||||
assert(_stack->count() == 1);
|
|
||||||
|
|
||||||
MediaDirectoryFormView* formView = new MediaDirectoryFormView(_stack);
|
|
||||||
formView->completed().connect(this, &MediaDirectories::handleMediaDirectoryFormCompleted);
|
|
||||||
|
|
||||||
_stack->setCurrentIndex(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectories::handleMediaDirectoryFormCompleted(bool changed)
|
|
||||||
{
|
|
||||||
_stack->setCurrentIndex(0);
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
// Refresh the user table if a change has been made
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
// Emit something changed in the settings
|
|
||||||
_sigChanged.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the form view
|
|
||||||
delete _stack->widget(1);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
} // namespace Settings
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_MEDIA_DIRECTORIES_HPP
|
|
||||||
#define UI_SETTINGS_MEDIA_DIRECTORIES_HPP
|
|
||||||
|
|
||||||
#include <Wt/WStackedWidget>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTable>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
#include "database/MediaDirectory.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class MediaDirectories : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
MediaDirectories(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
Wt::Signal<void>& changed() { return _sigChanged; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<void> _sigChanged;
|
|
||||||
|
|
||||||
void handleMediaDirectoryFormCompleted(bool changed);
|
|
||||||
|
|
||||||
void handleDelMediaDirectory(boost::filesystem::path p, Database::MediaDirectory::Type type);
|
|
||||||
void handleCreateMediaDirectory(void);
|
|
||||||
|
|
||||||
Wt::WStackedWidget* _stack;
|
|
||||||
Wt::WTable* _table;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
} // namespace Settings
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WStringListModel>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "database/MediaDirectory.hpp"
|
|
||||||
|
|
||||||
#include "common/DirectoryValidator.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsMediaDirectoryFormView.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
// TODO validate if directory already exists
|
|
||||||
class MediaDirectoryFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field PathField;
|
|
||||||
static const Field TypeField;
|
|
||||||
|
|
||||||
MediaDirectoryFormModel(Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent)
|
|
||||||
{
|
|
||||||
initializeModels();
|
|
||||||
|
|
||||||
addField(PathField);
|
|
||||||
addField(TypeField);
|
|
||||||
|
|
||||||
DirectoryValidator* dirValidator = new DirectoryValidator();
|
|
||||||
dirValidator->setMandatory(true);
|
|
||||||
setValidator(PathField, dirValidator);
|
|
||||||
setValidator(TypeField, new Wt::WValidator(true)); // mandatory
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WAbstractItemModel *typeModel() { return _typeModel; }
|
|
||||||
|
|
||||||
bool saveData(Wt::WString& error)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Database::MediaDirectory::Type type
|
|
||||||
= (valueText(TypeField) == "Audio") ? Database::MediaDirectory::Audio : Database::MediaDirectory::Video;
|
|
||||||
|
|
||||||
if (Database::MediaDirectory::get(DboSession(), valueText(PathField).toUTF8(), type))
|
|
||||||
{
|
|
||||||
error = "This Path/Type already exists!";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::MediaDirectory::create(DboSession(), valueText(PathField).toUTF8(), type);
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& exception)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void initializeModels()
|
|
||||||
{
|
|
||||||
_typeModel = new Wt::WStringListModel(this);
|
|
||||||
_typeModel->addString("Audio");
|
|
||||||
_typeModel->addString("Video");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string _userId;
|
|
||||||
Wt::WStringListModel* _typeModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field MediaDirectoryFormModel::PathField = "path";
|
|
||||||
const Wt::WFormModel::Field MediaDirectoryFormModel::TypeField = "type";
|
|
||||||
|
|
||||||
MediaDirectoryFormView::MediaDirectoryFormView(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_model = new MediaDirectoryFormModel(this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-new-media-directory"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
_applyInfo = new Wt::WText();
|
|
||||||
_applyInfo->setInline(false);
|
|
||||||
_applyInfo->hide();
|
|
||||||
bindWidget("apply-info", _applyInfo);
|
|
||||||
|
|
||||||
// Path
|
|
||||||
Wt::WLineEdit* pathEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(MediaDirectoryFormModel::PathField, pathEdit);
|
|
||||||
pathEdit->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Type
|
|
||||||
Wt::WComboBox *typeCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(MediaDirectoryFormModel::TypeField, typeCB);
|
|
||||||
typeCB->setModel(_model->typeModel());
|
|
||||||
typeCB->changed().connect(_applyInfo, &Wt::WWidget::hide);
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
bindString("title", "Add media folder");
|
|
||||||
|
|
||||||
Wt::WPushButton *saveButton = new Wt::WPushButton("Add");
|
|
||||||
saveButton->setStyleClass("btn-primary");
|
|
||||||
bindWidget("save-button", saveButton);
|
|
||||||
saveButton->clicked().connect(this, &MediaDirectoryFormView::processSave);
|
|
||||||
|
|
||||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Cancel");
|
|
||||||
bindWidget("cancel-button", cancelButton);
|
|
||||||
cancelButton->clicked().connect(this, &MediaDirectoryFormView::processCancel);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectoryFormView::processCancel()
|
|
||||||
{
|
|
||||||
// parent widget will delete this widget
|
|
||||||
completed().emit(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
MediaDirectoryFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
_applyInfo->show();
|
|
||||||
if (_model->validate())
|
|
||||||
{
|
|
||||||
Wt::WString error;
|
|
||||||
// commit model into DB
|
|
||||||
if (_model->saveData(error) ) {
|
|
||||||
completed().emit(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters: ") + error);
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_applyInfo->setText( Wt::WString::fromUTF8("Cannot apply new parameters!"));
|
|
||||||
_applyInfo->setStyleClass("alert alert-danger");
|
|
||||||
}
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_MEDIA_DIR_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_MEDIA_DIR_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class MediaDirectoryFormModel;
|
|
||||||
|
|
||||||
class MediaDirectoryFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
MediaDirectoryFormView(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
// Signal emitted once the form is completed
|
|
||||||
Wt::Signal<bool>& completed() { return _sigCompleted; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void processCancel(); // reload from DB
|
|
||||||
void processSave(); // commit into DB
|
|
||||||
|
|
||||||
Wt::Signal<bool> _sigCompleted;
|
|
||||||
MediaDirectoryFormModel* _model;
|
|
||||||
Wt::WText* _applyInfo;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/WStringListModel>
|
|
||||||
#include <Wt/WFormModel>
|
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WCheckBox>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/Auth/Identity>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "common/Validators.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsUserFormView.hpp"
|
|
||||||
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class UserFormModel : public Wt::WFormModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
// Associate each field with a unique string literal.
|
|
||||||
static const Field NameField;
|
|
||||||
static const Field EmailField;
|
|
||||||
static const Field PasswordField;
|
|
||||||
static const Field PasswordConfirmField;
|
|
||||||
static const Field AdminField;
|
|
||||||
static const Field AudioBitrateLimitField;
|
|
||||||
static const Field VideoBitrateLimitField;
|
|
||||||
|
|
||||||
UserFormModel(std::string userId, Wt::WObject *parent = 0)
|
|
||||||
: Wt::WFormModel(parent),
|
|
||||||
_userId(userId)
|
|
||||||
{
|
|
||||||
initializeModels();
|
|
||||||
|
|
||||||
addField(NameField);
|
|
||||||
addField(EmailField);
|
|
||||||
addField(PasswordField);
|
|
||||||
addField(PasswordConfirmField);
|
|
||||||
addField(AdminField);
|
|
||||||
addField(AudioBitrateLimitField);
|
|
||||||
addField(VideoBitrateLimitField);
|
|
||||||
|
|
||||||
setValidator(NameField, createNameValidator());
|
|
||||||
setValidator(EmailField, createEmailValidator());
|
|
||||||
// If creating a user, passwords are mandatory
|
|
||||||
if (_userId.empty())
|
|
||||||
{
|
|
||||||
setValidator(PasswordField, new Wt::WValidator(true)); // mandatory
|
|
||||||
setValidator(PasswordConfirmField, new Wt::WValidator(true)); // mandatory
|
|
||||||
}
|
|
||||||
setValidator(AudioBitrateLimitField, new Wt::WValidator(true)); // mandatory
|
|
||||||
setValidator(VideoBitrateLimitField, new Wt::WValidator(true)); // mandatory
|
|
||||||
|
|
||||||
// populate the model with initial data
|
|
||||||
loadData(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WAbstractItemModel *audioBitrateModel() { return _audioBitrateModel; }
|
|
||||||
Wt::WAbstractItemModel *videoBitrateModel() { return _videoBitrateModel; }
|
|
||||||
|
|
||||||
void loadData(std::string userId)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!userId.empty())
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( userId );
|
|
||||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
|
||||||
|
|
||||||
Wt::Auth::User currentUser = CurrentAuthUser();
|
|
||||||
|
|
||||||
if (user && authUser.isValid())
|
|
||||||
{
|
|
||||||
if (user->isAdmin())
|
|
||||||
{
|
|
||||||
setValue(AdminField, true);
|
|
||||||
|
|
||||||
// We can cannot remove admin rights to ourselves
|
|
||||||
if (currentUser == authUser)
|
|
||||||
setReadOnly(AdminField, true);
|
|
||||||
|
|
||||||
// if the user is admin, no need to limit it
|
|
||||||
setReadOnly(AudioBitrateLimitField, true);
|
|
||||||
setValidator(AudioBitrateLimitField, nullptr);
|
|
||||||
|
|
||||||
setReadOnly(VideoBitrateLimitField, true);
|
|
||||||
setValidator(VideoBitrateLimitField, nullptr);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setValue(AudioBitrateLimitField, user->getMaxAudioBitrate() / 1000); // in kbps
|
|
||||||
setValue(VideoBitrateLimitField, user->getMaxVideoBitrate() / 1000); // in kbps
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue(NameField, authUser.identity(Wt::Auth::Identity::LoginName));
|
|
||||||
if (!authUser.email().empty())
|
|
||||||
setValue(EmailField, authUser.email());
|
|
||||||
else
|
|
||||||
setValue(EmailField, authUser.unverifiedEmail());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool saveData()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
if (_userId.empty())
|
|
||||||
{
|
|
||||||
// Create user
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().registerNew();
|
|
||||||
Database::User::pointer user = DbHandler().getUser(authUser);
|
|
||||||
|
|
||||||
// Account
|
|
||||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
|
||||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
|
||||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
|
||||||
|
|
||||||
// Access
|
|
||||||
{
|
|
||||||
boost::any v = value(AdminField);
|
|
||||||
if (!v.empty() && boost::any_cast<bool>(v) == true)
|
|
||||||
user.modify()->setAdmin( true );
|
|
||||||
else
|
|
||||||
user.modify()->setAdmin( false );
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isReadOnly(AudioBitrateLimitField))
|
|
||||||
user.modify()->setMaxAudioBitrate( Wt::asNumber(value(AudioBitrateLimitField)) * 1000); // in bps
|
|
||||||
|
|
||||||
if (!isReadOnly(VideoBitrateLimitField))
|
|
||||||
user.modify()->setMaxVideoBitrate( Wt::asNumber(value(VideoBitrateLimitField)) * 1000); // in bps
|
|
||||||
|
|
||||||
transaction.commit();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Update user
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId(_userId);
|
|
||||||
Database::User::pointer user = DbHandler().getUser( authUser );
|
|
||||||
|
|
||||||
// user may have been deleted by someone else
|
|
||||||
if (!authUser.isValid()) {
|
|
||||||
LMS_LOG(UI, ERROR) << "user identity does not exist!";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if(!user)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "User not found!";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account
|
|
||||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(NameField));
|
|
||||||
authUser.setEmail(valueText(EmailField).toUTF8());
|
|
||||||
|
|
||||||
// Password
|
|
||||||
if (!valueText(PasswordField).empty())
|
|
||||||
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
|
|
||||||
|
|
||||||
// Access
|
|
||||||
if (!isReadOnly(AdminField))
|
|
||||||
{
|
|
||||||
boost::any v = value(AdminField);
|
|
||||||
if (!v.empty() && boost::any_cast<bool>(v) == true)
|
|
||||||
user.modify()->setAdmin( true );
|
|
||||||
else
|
|
||||||
user.modify()->setAdmin( false );
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isReadOnly(AudioBitrateLimitField))
|
|
||||||
user.modify()->setMaxAudioBitrate( Wt::asNumber(value(AudioBitrateLimitField)) * 1000); // in bps
|
|
||||||
|
|
||||||
if (!isReadOnly(VideoBitrateLimitField))
|
|
||||||
user.modify()->setMaxVideoBitrate( Wt::asNumber(value(VideoBitrateLimitField)) * 1000); // in bps
|
|
||||||
|
|
||||||
transaction.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& exception)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Dbo exception: " << exception.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool validateField(Field field)
|
|
||||||
{
|
|
||||||
Wt::WString error;
|
|
||||||
|
|
||||||
if (field == NameField)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
// Must be unique since used as LoginIdentity
|
|
||||||
Wt::Auth::User user = DbHandler().getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, valueText(field));
|
|
||||||
if (user.isValid() && user.id() != _userId)
|
|
||||||
error = "Already exists";
|
|
||||||
else
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
else if (field == PasswordField)
|
|
||||||
{
|
|
||||||
// Password is mandatory if we create the user
|
|
||||||
if (!valueText(PasswordField).empty())
|
|
||||||
{
|
|
||||||
// Evaluate the strength of the password
|
|
||||||
Wt::Auth::AbstractPasswordService::StrengthValidatorResult res
|
|
||||||
= Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
|
||||||
valueText(NameField),
|
|
||||||
valueText(EmailField).toUTF8());
|
|
||||||
|
|
||||||
if (!res.isValid())
|
|
||||||
error = res.message();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
else if (field == PasswordConfirmField)
|
|
||||||
{
|
|
||||||
if (validation(PasswordField).state() == Wt::WValidator::Valid)
|
|
||||||
{
|
|
||||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
|
||||||
error = Wt::WString::tr("Wt.Auth.passwords-dont-match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Apply validators
|
|
||||||
return Wt::WFormModel::validateField(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error));
|
|
||||||
|
|
||||||
return validation(field).state() == Wt::WValidator::Valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void initializeModels()
|
|
||||||
{
|
|
||||||
// AUDIO
|
|
||||||
_audioBitrateModel = new Wt::WStringListModel(this);
|
|
||||||
BOOST_FOREACH(std::size_t bitrate, Database::User::audioBitrates)
|
|
||||||
_audioBitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps
|
|
||||||
|
|
||||||
// VIDEO
|
|
||||||
_videoBitrateModel = new Wt::WStringListModel(this);
|
|
||||||
BOOST_FOREACH(std::size_t bitrate, Database::User::videoBitrates)
|
|
||||||
_videoBitrateModel->addString( Wt::WString("{1}").arg( bitrate / 1000 ) ); // in kbps
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string _userId;
|
|
||||||
Wt::WStringListModel* _audioBitrateModel;
|
|
||||||
Wt::WStringListModel* _videoBitrateModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wt::WFormModel::Field UserFormModel::NameField = "name";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::EmailField = "email";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::PasswordField = "password";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::PasswordConfirmField = "password-confirm";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::AdminField = "admin";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::AudioBitrateLimitField = "audio-bitrate-limit";
|
|
||||||
const Wt::WFormModel::Field UserFormModel::VideoBitrateLimitField = "video-bitrate-limit";
|
|
||||||
|
|
||||||
|
|
||||||
UserFormView::UserFormView(std::string userId, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WTemplateFormView(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_model = new UserFormModel(userId, this);
|
|
||||||
|
|
||||||
setTemplateText(tr("settings-new-user"));
|
|
||||||
addFunction("id", &WTemplate::Functions::id);
|
|
||||||
addFunction("block", &WTemplate::Functions::id);
|
|
||||||
|
|
||||||
// Name
|
|
||||||
setFormWidget(UserFormModel::NameField, new Wt::WLineEdit());
|
|
||||||
|
|
||||||
// Email
|
|
||||||
setFormWidget(UserFormModel::EmailField, new Wt::WLineEdit());
|
|
||||||
|
|
||||||
// Password
|
|
||||||
Wt::WLineEdit* passwordEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(UserFormModel::PasswordField, passwordEdit );
|
|
||||||
passwordEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
|
|
||||||
// Password confirmation
|
|
||||||
Wt::WLineEdit* passwordConfirmEdit = new Wt::WLineEdit();
|
|
||||||
setFormWidget(UserFormModel::PasswordConfirmField, passwordConfirmEdit);
|
|
||||||
passwordConfirmEdit->setEchoMode(Wt::WLineEdit::Password);
|
|
||||||
|
|
||||||
bindString("access", "Access");
|
|
||||||
|
|
||||||
// Admin Field
|
|
||||||
setFormWidget(UserFormModel::AdminField, new Wt::WCheckBox());
|
|
||||||
|
|
||||||
// AudioBitrate
|
|
||||||
Wt::WComboBox *audioBitrateCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(UserFormModel::AudioBitrateLimitField, audioBitrateCB);
|
|
||||||
audioBitrateCB->setStyleClass("span2");
|
|
||||||
audioBitrateCB->setModel(_model->audioBitrateModel());
|
|
||||||
|
|
||||||
// VideoBitrate
|
|
||||||
Wt::WComboBox *videoBitrateCB = new Wt::WComboBox();
|
|
||||||
setFormWidget(UserFormModel::VideoBitrateLimitField, videoBitrateCB);
|
|
||||||
videoBitrateCB->setStyleClass("span2");
|
|
||||||
videoBitrateCB->setModel(_model->videoBitrateModel());
|
|
||||||
|
|
||||||
// Title & Buttons
|
|
||||||
Wt::WString title;
|
|
||||||
if (userId.empty()) {
|
|
||||||
title = Wt::WString("Create user");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Wt::Dbo::Transaction transaction (DboSession());
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( userId );
|
|
||||||
|
|
||||||
Wt::WString userName;
|
|
||||||
if (authUser.isValid())
|
|
||||||
userName = authUser.identity(Wt::Auth::Identity::LoginName);
|
|
||||||
else
|
|
||||||
; // TODO display user deleted and close the widget
|
|
||||||
|
|
||||||
title = Wt::WString("Edit user {1}").arg(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindString("title", title);
|
|
||||||
|
|
||||||
Wt::WPushButton *saveButton = new Wt::WPushButton();
|
|
||||||
if (userId.empty()) {
|
|
||||||
saveButton->setText("Create user");
|
|
||||||
saveButton->setStyleClass("btn-success");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
saveButton->setText("Save");
|
|
||||||
saveButton->setStyleClass("btn-primary");
|
|
||||||
}
|
|
||||||
bindWidget("save-button", saveButton);
|
|
||||||
saveButton->clicked().connect(this, &UserFormView::processSave);
|
|
||||||
|
|
||||||
Wt::WPushButton *cancelButton = new Wt::WPushButton("Cancel");
|
|
||||||
bindWidget("cancel-button", cancelButton);
|
|
||||||
cancelButton->clicked().connect(this, &UserFormView::processCancel);
|
|
||||||
|
|
||||||
updateView(_model);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
UserFormView::processCancel()
|
|
||||||
{
|
|
||||||
// parent widget will delete this widget
|
|
||||||
completed().emit(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
UserFormView::processSave()
|
|
||||||
{
|
|
||||||
updateModel(_model);
|
|
||||||
|
|
||||||
|
|
||||||
if (_model->validate())
|
|
||||||
{
|
|
||||||
// commit model into DB
|
|
||||||
if (_model->saveData() )
|
|
||||||
{
|
|
||||||
// parent widget will delete this widget
|
|
||||||
completed().emit(true);
|
|
||||||
}
|
|
||||||
// else TODO display a nice error message
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
updateView(_model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_USER_FORM_VIEW_HPP
|
|
||||||
#define UI_SETTINGS_USER_FORM_VIEW_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTemplateFormView>
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class UserFormModel;
|
|
||||||
|
|
||||||
class UserFormView : public Wt::WTemplateFormView
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
UserFormView(std::string userId, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
// Signal emitted once the form is completed
|
|
||||||
Wt::Signal<bool>& completed() { return _sigCompleted; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
Wt::Signal<bool> _sigCompleted;
|
|
||||||
|
|
||||||
void processSave();
|
|
||||||
void processCancel();
|
|
||||||
|
|
||||||
UserFormModel* _model;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Settings
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
#include <Wt/WGroupBox>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WMessageBox>
|
|
||||||
|
|
||||||
#include <Wt/Auth/Identity>
|
|
||||||
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
|
|
||||||
#include "SettingsUserFormView.hpp"
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "SettingsUsers.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
Users::Users(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
// Stack two widgets:
|
|
||||||
_stack = new Wt::WStackedWidget(this);
|
|
||||||
|
|
||||||
// 1/ the user table view
|
|
||||||
{
|
|
||||||
Wt::WGroupBox *container = new Wt::WGroupBox("Users", _stack);
|
|
||||||
|
|
||||||
_table = new Wt::WTable(container);
|
|
||||||
|
|
||||||
_table->addStyleClass("table form-inline");
|
|
||||||
|
|
||||||
_table->toggleStyleClass("table-hover", true);
|
|
||||||
_table->toggleStyleClass("table-striped", true);
|
|
||||||
|
|
||||||
_table->setHeaderCount(1);
|
|
||||||
|
|
||||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("#"));
|
|
||||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Name"));
|
|
||||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("e-Mail"));
|
|
||||||
_table->elementAt(0, 3)->addWidget(new Wt::WText("Admin"));
|
|
||||||
|
|
||||||
Wt::WPushButton* addBtn = new Wt::WPushButton("Add User");
|
|
||||||
addBtn->setStyleClass("btn-success");
|
|
||||||
container->addWidget( addBtn );
|
|
||||||
addBtn->clicked().connect(boost::bind(&Users::handleCreateUser, this, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Users::refresh(void)
|
|
||||||
{
|
|
||||||
|
|
||||||
assert(_table->rowCount() > 0);
|
|
||||||
for (int i = _table->rowCount() - 1; i > 0; --i)
|
|
||||||
_table->deleteRow(i);
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
const Wt::Auth::User& currentUser = CurrentAuthUser();
|
|
||||||
|
|
||||||
std::vector<Database::User::pointer> users = Database::User::getAll(DboSession());
|
|
||||||
|
|
||||||
std::size_t userIndex = 1;
|
|
||||||
for (std::size_t i = 0; i < users.size(); ++i)
|
|
||||||
{
|
|
||||||
|
|
||||||
std::string userId = Database::User::getId(users[i]);
|
|
||||||
|
|
||||||
Wt::Auth::User authUser;
|
|
||||||
|
|
||||||
// Hack try/catch here since it may fail!
|
|
||||||
try {
|
|
||||||
authUser = DbHandler().getUserDatabase().findWithId( userId );
|
|
||||||
}
|
|
||||||
catch(Wt::Dbo::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, ERROR) << "Caught exception when getting userId=" << userId << ": " << e.code();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authUser.isValid()) {
|
|
||||||
LMS_LOG(UI, ERROR) << "Users::refresh: skipping invalid userId = " << userId;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_table->elementAt(userIndex, 0)->addWidget(new Wt::WText( Wt::WString::fromUTF8("{1}").arg(userIndex)));
|
|
||||||
_table->elementAt(userIndex, 1)->addWidget(new Wt::WText( authUser.identity(Wt::Auth::Identity::LoginName)) );
|
|
||||||
|
|
||||||
Wt::WText* email = new Wt::WText();
|
|
||||||
if (!authUser.email().empty()) {
|
|
||||||
email->setText(authUser.email());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
email->setStyleClass("alert-danger");
|
|
||||||
email->setText(authUser.unverifiedEmail());
|
|
||||||
}
|
|
||||||
_table->elementAt(userIndex, 2)->addWidget( email ) ;
|
|
||||||
_table->elementAt(userIndex, 3)->addWidget(new Wt::WText( users[i]->isAdmin() ? "Yes" : "No" ));
|
|
||||||
|
|
||||||
Wt::WPushButton* editBtn = new Wt::WPushButton("Edit");
|
|
||||||
_table->elementAt(userIndex, 4)->addWidget(editBtn);
|
|
||||||
editBtn->clicked().connect(boost::bind( &Users::handleCreateUser, this, userId));
|
|
||||||
|
|
||||||
if (currentUser != authUser)
|
|
||||||
{
|
|
||||||
Wt::WPushButton* delBtn = new Wt::WPushButton("Delete");
|
|
||||||
delBtn->setStyleClass("btn-danger");
|
|
||||||
delBtn->setMargin(5, Wt::Left);
|
|
||||||
_table->elementAt(userIndex, 4)->addWidget(delBtn);
|
|
||||||
delBtn->clicked().connect(boost::bind( &Users::handleDelUser, this, authUser.identity(Wt::Auth::Identity::LoginName), userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
++userIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Users::handleDelUser(Wt::WString loginNameIdentity, std::string id)
|
|
||||||
{
|
|
||||||
Wt::WMessageBox *messageBox = new Wt::WMessageBox
|
|
||||||
("Delete User",
|
|
||||||
Wt::WString( "Deleting user '{1}'?").arg(loginNameIdentity),
|
|
||||||
Wt::Question, Wt::Yes | Wt::No);
|
|
||||||
|
|
||||||
messageBox->setModal(true);
|
|
||||||
|
|
||||||
messageBox->buttonClicked().connect(std::bind([=] () {
|
|
||||||
if (messageBox->buttonResult() == Wt::Yes)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
// Delete the user
|
|
||||||
Wt::Auth::User authUser = DbHandler().getUserDatabase().findWithId( id );
|
|
||||||
|
|
||||||
DbHandler().getUserDatabase().deleteUser( authUser );
|
|
||||||
|
|
||||||
Database::User::pointer user = Database::User::getById(DboSession(), id);
|
|
||||||
if (user)
|
|
||||||
user.remove();
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete messageBox;
|
|
||||||
}));
|
|
||||||
|
|
||||||
messageBox->show();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Users::handleCreateUser(std::string id)
|
|
||||||
{
|
|
||||||
assert(_stack->count() == 1);
|
|
||||||
|
|
||||||
UserFormView* userFormView = new UserFormView(id, _stack);
|
|
||||||
userFormView->completed().connect(this, &Users::handleUserFormCompleted);
|
|
||||||
|
|
||||||
_stack->setCurrentIndex(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Users::handleUserFormCompleted(bool changed)
|
|
||||||
{
|
|
||||||
_stack->setCurrentIndex(0);
|
|
||||||
|
|
||||||
// Refresh the user table if a change has been made
|
|
||||||
if (changed)
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
// Delete the form view
|
|
||||||
delete _stack->widget(1);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
} // namespace Settings
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef UI_SETTINGS_USERS_HPP
|
|
||||||
#define UI_SETTINGS_USERS_HPP
|
|
||||||
|
|
||||||
#include <Wt/WStackedWidget>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WTable>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Settings {
|
|
||||||
|
|
||||||
class Users : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Users(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void handleUserFormCompleted(bool changed);
|
|
||||||
|
|
||||||
void handleDelUser(Wt::WString loginNameIdentity, std::string id);
|
|
||||||
void handleCreateUser(std::string id); // set the id in order to edit the user
|
|
||||||
|
|
||||||
Wt::WStackedWidget* _stack;
|
|
||||||
Wt::WTable* _table;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
} // namespace Settings
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/foreach.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
|
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WFileResource>
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "VideoDatabaseWidget.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
VideoDatabaseWidget::VideoDatabaseWidget(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
_table = new Wt::WTable( this );
|
|
||||||
_table->setHeaderCount(1);
|
|
||||||
|
|
||||||
_table->addStyleClass("table form-inline");
|
|
||||||
|
|
||||||
_table->toggleStyleClass("table-hover", true);
|
|
||||||
_table->toggleStyleClass("table-striped", true);
|
|
||||||
|
|
||||||
updateView( boost::filesystem::path(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoDatabaseWidget::addHeader(void)
|
|
||||||
{
|
|
||||||
_table->elementAt(0, 0)->addWidget(new Wt::WText("Name"));
|
|
||||||
_table->elementAt(0, 1)->addWidget(new Wt::WText("Duration"));
|
|
||||||
_table->elementAt(0, 2)->addWidget(new Wt::WText("Action"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoDatabaseWidget::addDirectory(const std::string& name, boost::filesystem::path path, size_t depth)
|
|
||||||
{
|
|
||||||
|
|
||||||
int row = _table->rowCount();
|
|
||||||
|
|
||||||
new Wt::WText(Wt::WString::fromUTF8( name ), _table->elementAt(row, 0));
|
|
||||||
new Wt::WText(Wt::WString::fromUTF8( " " ), _table->elementAt(row, 1));
|
|
||||||
|
|
||||||
Wt::WPushButton* btn = new Wt::WPushButton(Wt::WString::fromUTF8( "Open"), _table->elementAt(row, 2));
|
|
||||||
|
|
||||||
btn->clicked().connect(std::bind([=] () {
|
|
||||||
updateView(path, depth);
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoDatabaseWidget::addVideo(const std::string& name, const boost::posix_time::time_duration& duration, const boost::filesystem::path& path)
|
|
||||||
{
|
|
||||||
|
|
||||||
int row = _table->rowCount();
|
|
||||||
|
|
||||||
new Wt::WText(Wt::WString::fromUTF8( name ), _table->elementAt(row, 0));
|
|
||||||
new Wt::WText(Wt::WString::fromUTF8( boost::posix_time::to_simple_string( duration )), _table->elementAt(row, 1));
|
|
||||||
|
|
||||||
Wt::WPushButton* btn = new Wt::WPushButton(Wt::WString::fromUTF8( "Play"), _table->elementAt(row, 2));
|
|
||||||
|
|
||||||
btn->clicked().connect(std::bind([=] () {
|
|
||||||
playVideo().emit(path);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoDatabaseWidget::updateView(boost::filesystem::path directory, size_t depth)
|
|
||||||
{
|
|
||||||
_table->clear();
|
|
||||||
addHeader();
|
|
||||||
|
|
||||||
// If directory is not valid, add the root Media Directories
|
|
||||||
if (depth == 0)
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction ( DboSession() );
|
|
||||||
|
|
||||||
std::vector<Database::MediaDirectory::pointer> dirs
|
|
||||||
= Database::MediaDirectory::getByType(DboSession(), Database::MediaDirectory::Video);
|
|
||||||
|
|
||||||
BOOST_FOREACH(Database::MediaDirectory::pointer dir, dirs)
|
|
||||||
{
|
|
||||||
addDirectory( dir->getPath().filename().string(),
|
|
||||||
dir->getPath(),
|
|
||||||
depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
addDirectory( "..", directory.parent_path(), depth - 1);
|
|
||||||
|
|
||||||
// Iterators over files in the directory
|
|
||||||
// If directory, add a directory entry
|
|
||||||
// If video, add a video entry
|
|
||||||
std::vector<boost::filesystem::path> paths;
|
|
||||||
std::copy(boost::filesystem::directory_iterator(directory), boost::filesystem::directory_iterator(), std::back_inserter(paths));
|
|
||||||
|
|
||||||
std::sort(paths.begin(), paths.end());
|
|
||||||
|
|
||||||
BOOST_FOREACH(const boost::filesystem::path& path, paths)
|
|
||||||
{
|
|
||||||
if (boost::filesystem::is_directory(path))
|
|
||||||
addDirectory( path.filename().string(), path, depth + 1);
|
|
||||||
else if (boost::filesystem::is_regular(path) )
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction ( DboSession() );
|
|
||||||
|
|
||||||
Database::Video::pointer video = Database::Video::getByPath( DboSession(), path);
|
|
||||||
if (video)
|
|
||||||
addVideo( video->getName(), video->getDuration(), path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef VIDEO_DB_WIDGET_HPP
|
|
||||||
#define VIDEO_DB_WIDGET_HPP
|
|
||||||
|
|
||||||
#include <Wt/WTable>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class VideoDatabaseWidget : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
VideoDatabaseWidget(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
Wt::Signal< boost::filesystem::path >& playVideo() { return _playVideo; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void addHeader(void);
|
|
||||||
void addDirectory(const std::string& name, boost::filesystem::path path, size_t depth);
|
|
||||||
void addVideo(const std::string& name, const boost::posix_time::time_duration& duration, const boost::filesystem::path& path);
|
|
||||||
|
|
||||||
void updateView(boost::filesystem::path directory, size_t depth);
|
|
||||||
|
|
||||||
Wt::Signal< boost::filesystem::path > _playVideo;
|
|
||||||
|
|
||||||
Wt::WTable* _table;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp> //include all types plus i/o
|
|
||||||
|
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
|
|
||||||
#include "VideoParametersDialog.hpp"
|
|
||||||
|
|
||||||
#include "VideoMediaPlayerWidget.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
Wt::WMediaPlayer::Encoding
|
|
||||||
AvEncoding_to_WtEncoding(Av::Encoding encoding)
|
|
||||||
{
|
|
||||||
switch( encoding )
|
|
||||||
{
|
|
||||||
case Av::Encoding::OGA: return Wt::WMediaPlayer::OGA;
|
|
||||||
case Av::Encoding::OGV: return Wt::WMediaPlayer::OGV;
|
|
||||||
case Av::Encoding::MP3: return Wt::WMediaPlayer::MP3;
|
|
||||||
case Av::Encoding::WEBMA: return Wt::WMediaPlayer::WEBMA;
|
|
||||||
case Av::Encoding::WEBMV: return Wt::WMediaPlayer::WEBMV;
|
|
||||||
case Av::Encoding::FLA: return Wt::WMediaPlayer::FLA;
|
|
||||||
case Av::Encoding::FLV: return Wt::WMediaPlayer::FLV;
|
|
||||||
case Av::Encoding::M4A: return Wt::WMediaPlayer::M4A;
|
|
||||||
case Av::Encoding::M4V: return Wt::WMediaPlayer::M4V;
|
|
||||||
}
|
|
||||||
assert(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VideoMediaPlayerWidget::VideoMediaPlayerWidget( const Av::MediaFile& mediaFile, Av::TranscodeParameters parameters, Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent),
|
|
||||||
_mediaResource(nullptr),
|
|
||||||
_currentParameters(parameters),
|
|
||||||
_dialog(nullptr)
|
|
||||||
{
|
|
||||||
_mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Video, this );
|
|
||||||
// _mediaPlayer->setAlternativeContent (new Wt::WText("You don't have HTML5 audio support!"));
|
|
||||||
// _mediaPlayer->setOptions( Wt::WMediaPlayer::Autoplay );
|
|
||||||
// _mediaPlayer->addSource( Wt::WMediaPlayer::WEBMV, "" );
|
|
||||||
|
|
||||||
{
|
|
||||||
Wt::WContainerWidget *container = new Wt::WContainerWidget(this);
|
|
||||||
|
|
||||||
_playBtn = new Wt::WPushButton("Play", container );
|
|
||||||
_pauseBtn = new Wt::WPushButton("Pause", container );
|
|
||||||
|
|
||||||
_curTime = new Wt::WText(container);
|
|
||||||
_timeSlider = new Wt::WSlider( container );
|
|
||||||
_duration = new Wt::WText(container);
|
|
||||||
|
|
||||||
_volumeSlider = new Wt::WSlider( container );
|
|
||||||
_volumeSlider->setRange(0,100);
|
|
||||||
_volumeSlider->setValue(_mediaPlayer->volume() * 100);
|
|
||||||
|
|
||||||
_mediaPlayer->setControlsWidget( container );
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn);
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn);
|
|
||||||
|
|
||||||
_mediaPlayer->setText( Wt::WMediaPlayer::CurrentTime, _curTime);
|
|
||||||
_mediaPlayer->setText( Wt::WMediaPlayer::Duration, _duration);
|
|
||||||
|
|
||||||
_mediaPlayer->timeUpdated().connect(this, &VideoMediaPlayerWidget::handleTimeUpdated);
|
|
||||||
|
|
||||||
Wt::WPushButton* fullScreenButton = new Wt::WPushButton("Fullscreen", container);
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::FullScreen, fullScreenButton);
|
|
||||||
|
|
||||||
Wt::WPushButton* restoreScreenButton = new Wt::WPushButton("Restore screen", container);
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::RestoreScreen, restoreScreenButton);
|
|
||||||
|
|
||||||
_mediaPlayer->timeUpdated().connect(this, &VideoMediaPlayerWidget::handleTimeUpdated);
|
|
||||||
|
|
||||||
_timeSlider->valueChanged().connect(this, &VideoMediaPlayerWidget::handlePlayOffset);
|
|
||||||
_timeSlider->sliderMoved().connect(this, &VideoMediaPlayerWidget::handleSliderMoved);
|
|
||||||
|
|
||||||
_volumeSlider->sliderMoved().connect(this, &VideoMediaPlayerWidget::handleVolumeSliderMoved);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Wt::WPushButton* closeButton = new Wt::WPushButton("Close", this);
|
|
||||||
closeButton->clicked().connect( this, &VideoMediaPlayerWidget::handleClose );
|
|
||||||
|
|
||||||
Wt::WPushButton* parametersButton = new Wt::WPushButton("Parameters", this);
|
|
||||||
parametersButton->clicked().connect( this, &VideoMediaPlayerWidget::handleParametersEdit );
|
|
||||||
|
|
||||||
_currentFile = mediaFile.getPath();
|
|
||||||
|
|
||||||
load(parameters);
|
|
||||||
|
|
||||||
_timeSlider->setRange(0, mediaFile.getDuration().total_seconds() );
|
|
||||||
_duration->setText( boost::posix_time::to_simple_string( mediaFile.getDuration() ));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::load(Av::TranscodeParameters parameters)
|
|
||||||
{
|
|
||||||
_mediaPlayer->clearSources();
|
|
||||||
|
|
||||||
_currentParameters = parameters;
|
|
||||||
|
|
||||||
_mediaInternalLink.setResource( nullptr );
|
|
||||||
if (_mediaResource)
|
|
||||||
delete _mediaResource;
|
|
||||||
|
|
||||||
_mediaResource = new AvConvTranscodeStreamResource( _currentFile, parameters, this );
|
|
||||||
_mediaInternalLink.setResource( _mediaResource );
|
|
||||||
|
|
||||||
_mediaPlayer->addSource( AvEncoding_to_WtEncoding(parameters.getEncoding()), _mediaInternalLink );
|
|
||||||
|
|
||||||
_timeSlider->setValue( 0 );
|
|
||||||
|
|
||||||
|
|
||||||
_mediaPlayer->play();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handlePlayOffset(int offsetSecs)
|
|
||||||
{
|
|
||||||
_currentParameters.setOffset( boost::posix_time::seconds(offsetSecs) );
|
|
||||||
load( _currentParameters );
|
|
||||||
|
|
||||||
_timeSlider->setValue( offsetSecs );
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleSliderMoved(int value)
|
|
||||||
{
|
|
||||||
_curTime->setText( boost::posix_time::to_simple_string( boost::posix_time::seconds( value ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleTimeUpdated(void)
|
|
||||||
{
|
|
||||||
|
|
||||||
boost::posix_time::time_duration currentTime ( boost::posix_time::seconds( _mediaPlayer->currentTime() + _currentParameters.getOffset().total_seconds()));
|
|
||||||
|
|
||||||
_timeSlider->setValue( currentTime.total_seconds() );
|
|
||||||
_curTime->setText( boost::posix_time::to_simple_string( currentTime) );
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleVolumeSliderMoved(int value)
|
|
||||||
{
|
|
||||||
_mediaPlayer->setVolume( value / 100. );
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleClose(void)
|
|
||||||
{
|
|
||||||
_close.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleParametersEdit(void)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
_dialog = new VideoParametersDialog("Parameters");
|
|
||||||
_dialog->load(_currentParameters);
|
|
||||||
|
|
||||||
_dialog->show();
|
|
||||||
|
|
||||||
_dialog->finished().connect(this, &VideoMediaPlayerWidget::handleParametersDone);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoMediaPlayerWidget::handleParametersDone(Wt::WDialog::DialogCode code)
|
|
||||||
{
|
|
||||||
assert(_dialog != nullptr);
|
|
||||||
|
|
||||||
if (code == Wt::WDialog::Accepted)
|
|
||||||
{
|
|
||||||
_dialog->save( _currentParameters );
|
|
||||||
|
|
||||||
// TODO SYNC current offset with player?
|
|
||||||
// HACK use slider current value
|
|
||||||
handlePlayOffset( _timeSlider->value());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __VIDEO_MEDIA_PLAYER_WIDGET_HPP
|
|
||||||
#define __VIDEO_MEDIA_PLAYER_WIDGET_HPP
|
|
||||||
|
|
||||||
#include <Wt/WDialog>
|
|
||||||
#include <Wt/WSlider>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WLink>
|
|
||||||
|
|
||||||
#include "VideoParametersDialog.hpp"
|
|
||||||
|
|
||||||
#include "av/AvTranscoder.hpp"
|
|
||||||
#include "resource/AvConvTranscodeStreamResource.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class VideoMediaPlayerWidget : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
VideoMediaPlayerWidget( const Av::MediaFile& mediaFile, Av::TranscodeParameters parameters, Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
Wt::Signal<void>& close() { return _close; };
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
Wt::Signal<void> _close;
|
|
||||||
|
|
||||||
void load(Av::TranscodeParameters parameters);
|
|
||||||
|
|
||||||
// Player controls
|
|
||||||
void handlePlayOffset(int offsetSecs);
|
|
||||||
void handleTimeUpdated(void);
|
|
||||||
void handleSliderMoved(int value);
|
|
||||||
void handleFullscreen(void);
|
|
||||||
void handleVolumeSliderMoved(int value);
|
|
||||||
|
|
||||||
void handleClose(void);
|
|
||||||
|
|
||||||
void handleParametersEdit(void);
|
|
||||||
void handleParametersDone(Wt::WDialog::DialogCode);
|
|
||||||
|
|
||||||
boost::filesystem::path _currentFile;
|
|
||||||
|
|
||||||
// Core
|
|
||||||
Wt::WMediaPlayer* _mediaPlayer;
|
|
||||||
AvConvTranscodeStreamResource* _mediaResource;
|
|
||||||
Wt::WLink _mediaInternalLink;
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
Av::TranscodeParameters _currentParameters;
|
|
||||||
Wt::WPushButton* _playBtn;
|
|
||||||
Wt::WPushButton* _pauseBtn;
|
|
||||||
Wt::WSlider* _timeSlider;
|
|
||||||
Wt::WSlider* _volumeSlider;
|
|
||||||
Wt::WText* _curTime;
|
|
||||||
Wt::WText* _duration;
|
|
||||||
|
|
||||||
VideoParametersDialog* _dialog;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WLabel>
|
|
||||||
#include <Wt/WTable>
|
|
||||||
|
|
||||||
#include "VideoParametersDialog.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
static const std::list<Av::Stream::Type> streamTypes = {Av::Stream::Type::Video, Av::Stream::Type::Audio, Av::Stream::Type::Subtitle};
|
|
||||||
|
|
||||||
VideoParametersDialog::VideoParametersDialog(Wt::WString windowTitle, Wt::WDialog* parent)
|
|
||||||
: Wt::WDialog(windowTitle, parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
Wt::WTable* layout = new Wt::WTable(contents());
|
|
||||||
|
|
||||||
createStreamWidgets("Video", Av::Stream::Type::Video, layout);
|
|
||||||
createStreamWidgets("Audio", Av::Stream::Type::Audio, layout);
|
|
||||||
createStreamWidgets("Subtitles", Av::Stream::Type::Subtitle, layout);
|
|
||||||
|
|
||||||
Wt::WPushButton *ok = new Wt::WPushButton("Apply", contents());
|
|
||||||
ok->clicked().connect(this, &Wt::WDialog::accept);
|
|
||||||
|
|
||||||
Wt::WPushButton *cancel = new Wt::WPushButton("Cancel", contents());
|
|
||||||
cancel->clicked().connect(this, &Wt::WDialog::reject);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::createStreamWidgets(const Wt::WString& labelString, Av::Stream::Type type, Wt::WTable* layout)
|
|
||||||
{
|
|
||||||
int row = layout->rowCount();
|
|
||||||
|
|
||||||
Wt::WLabel* label = new Wt::WLabel(labelString);
|
|
||||||
Wt::WComboBox *combo = new Wt::WComboBox();
|
|
||||||
label->setBuddy(combo);
|
|
||||||
|
|
||||||
layout->elementAt(row, 0)->addWidget(label);
|
|
||||||
layout->elementAt(row, 1)->addWidget(combo);
|
|
||||||
|
|
||||||
Wt::WStringListModel* model = new Wt::WStringListModel(combo);
|
|
||||||
|
|
||||||
combo->setModel(model);
|
|
||||||
|
|
||||||
_streamSelection.insert( std::make_pair(type, std::make_pair(combo, model) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::handleApply()
|
|
||||||
{
|
|
||||||
_apply.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::addStreams(Wt::WStringListModel* model, const std::vector<Av::Stream>& streams)
|
|
||||||
{
|
|
||||||
for (const Av::Stream& stream : streams)
|
|
||||||
{
|
|
||||||
model->addString(stream.desc);
|
|
||||||
model->setData(model->rowCount(), 0, stream.id, Wt::UserRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::selectStream(const Wt::WStringListModel* model, int streamId, Wt::WComboBox* combo)
|
|
||||||
{
|
|
||||||
for (int idStream = 0; idStream < model->rowCount(); ++idStream)
|
|
||||||
{
|
|
||||||
int id = boost::any_cast<int>( model->data( model->index(idStream, 0), Wt::UserRole));
|
|
||||||
if (id == streamId)
|
|
||||||
{
|
|
||||||
combo->setCurrentIndex(idStream);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::load(const Av::MediaFile& mediaFile, Av::TranscodeParameters currentParameters)
|
|
||||||
{
|
|
||||||
// Populate the combox with the available streams
|
|
||||||
// And then show the selected one
|
|
||||||
for (Av::Stream::Type streamType : streamTypes)
|
|
||||||
{
|
|
||||||
Wt::WComboBox* combo = _streamSelection[streamType].first;
|
|
||||||
Wt::WStringListModel* model = _streamSelection[streamType].second;
|
|
||||||
|
|
||||||
addStreams(model, mediaFile.getStreams(streamType) );
|
|
||||||
|
|
||||||
// Pre Select if necessary
|
|
||||||
std::set<int> selectedStreams = currentParameters.getSelectedStreamIds();
|
|
||||||
for (Av::Stream stream : mediaFile.getStreams(streamType))
|
|
||||||
{
|
|
||||||
if (selectedStreams.find(stream.id) != selectedStreams.end())
|
|
||||||
{
|
|
||||||
selectStream(model, stream.id, combo);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoParametersDialog::save(Av::TranscodeParameters& parameters)
|
|
||||||
{
|
|
||||||
// Get stream selected, if any
|
|
||||||
for (Av::Stream::Type streamType : streamTypes)
|
|
||||||
{
|
|
||||||
Wt::WComboBox* combo = _streamSelection[streamType].first;
|
|
||||||
Wt::WStringListModel* model = _streamSelection[streamType].second;
|
|
||||||
|
|
||||||
if (combo->currentIndex() >= 0)
|
|
||||||
{
|
|
||||||
int streamId = boost::any_cast<int>( model->data(model->index(combo->currentIndex(), 0), Wt::UserRole));
|
|
||||||
|
|
||||||
parameters.addStream(streamId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include <Wt/WSignal>
|
|
||||||
#include <Wt/WDialog>
|
|
||||||
#include <Wt/WComboBox>
|
|
||||||
#include <Wt/WStringListModel>
|
|
||||||
#include <Wt/WString>
|
|
||||||
|
|
||||||
#include "av/AvTranscoder.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class VideoParametersDialog : public Wt::WDialog
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
// parameters to be edited
|
|
||||||
VideoParametersDialog(Wt::WString windowTitle, Wt::WDialog* parent = 0);
|
|
||||||
|
|
||||||
// Populates widget contents using these paramaters
|
|
||||||
void load(const Av::MediaFile& mediaFile, Av::TranscodeParameters currentParameters);
|
|
||||||
|
|
||||||
// Save widgets contents into these parameters
|
|
||||||
void save(Av::TranscodeParameters& parameters);
|
|
||||||
|
|
||||||
// Signal to be emitted if parameters are changed
|
|
||||||
Wt::Signal<void>& apply() { return _apply; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void handleApply(void);
|
|
||||||
|
|
||||||
// Stream handling
|
|
||||||
void createStreamWidgets(const Wt::WString& label, Av::Stream::Type type, Wt::WTable* layout);
|
|
||||||
void addStreams(Wt::WStringListModel* model, const std::vector<Av::Stream>& streams);
|
|
||||||
void selectStream(const Wt::WStringListModel* model, int streamId, Wt::WComboBox* combo);
|
|
||||||
|
|
||||||
Wt::Signal<void> _apply;
|
|
||||||
|
|
||||||
std::map<Av::Stream::Type, std::pair<Wt::WComboBox*, Wt::WStringListModel* > > _streamSelection;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WApplication>
|
|
||||||
#include <Wt/WEnvironment>
|
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "VideoWidget.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
VideoWidget::VideoWidget(Wt::WContainerWidget* parent )
|
|
||||||
: Wt::WContainerWidget(parent)
|
|
||||||
{
|
|
||||||
|
|
||||||
_videoDbWidget = new VideoDatabaseWidget(this);
|
|
||||||
|
|
||||||
_videoDbWidget->playVideo().connect(this, &VideoWidget::playVideo);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoWidget::search(const std::string& searchText)
|
|
||||||
{
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
VideoWidget::playVideo(boost::filesystem::path p)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, DEBUG) << "Want to play video " << p << "'";
|
|
||||||
|
|
||||||
std::size_t audioBitrate = 0;
|
|
||||||
std::size_t videoBitrate = 0;
|
|
||||||
|
|
||||||
// Get user preferences
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
audioBitrate = CurrentUser()->getMaxAudioBitrate();
|
|
||||||
videoBitrate = CurrentUser()->getMaxVideoBitrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Max bitrate set to " << videoBitrate << "/" << audioBitrate;
|
|
||||||
|
|
||||||
Av::MediaFile mediaFile(p);
|
|
||||||
|
|
||||||
if (!mediaFile.open())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!mediaFile.scan())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Av::TranscodeParameters parameters;
|
|
||||||
|
|
||||||
parameters.setEncoding( Av::Encoding::WEBMV );
|
|
||||||
|
|
||||||
VideoMediaPlayerWidget *mediaPlayer = new VideoMediaPlayerWidget(mediaFile, parameters, this);
|
|
||||||
|
|
||||||
mediaPlayer->close().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_videoDbWidget->setHidden(false);
|
|
||||||
delete mediaPlayer;
|
|
||||||
}));
|
|
||||||
|
|
||||||
_videoDbWidget->setHidden(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef VIDEO_WIDGET_HPP
|
|
||||||
#define VIDEO_WIDGET_HPP
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
|
|
||||||
#include "video/VideoMediaPlayerWidget.hpp"
|
|
||||||
#include "video/VideoDatabaseWidget.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
|
|
||||||
class VideoWidget : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
|
|
||||||
VideoWidget(Wt::WContainerWidget* parent = 0);
|
|
||||||
|
|
||||||
void search(const std::string& searchText);
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void backToList(void);
|
|
||||||
void playVideo(boost::filesystem::path p);
|
|
||||||
|
|
||||||
VideoDatabaseWidget* _videoDbWidget;
|
|
||||||
VideoMediaPlayerWidget* _mediaPlayer;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user