From 3de401c218d3719c7b95f3d587e53d79d155aa5a Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 11 Nov 2015 22:48:22 +0100 Subject: [PATCH 01/29] [DB] Better support of music brainz tags on some FLAC files --- src/metadata/AvFormat.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp index 18f305bc..36bc6c4f 100644 --- a/src/metadata/AvFormat.cpp +++ b/src/metadata/AvFormat.cpp @@ -150,11 +150,13 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items) items.insert( std::make_pair(MetaData::Type::Genres, genres)); } - else if (boost::iequals(it->first, "MusicBrainz Artist Id")) + else if (boost::iequals(it->first, "MusicBrainz Artist Id") + || boost::iequals(it->first, "MUSICBRAINZ_ARTISTID")) { items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, string_trim( string_to_utf8(it->second)) )); } - else if (boost::iequals(it->first, "MusicBrainz Album Id")) + else if (boost::iequals(it->first, "MusicBrainz Album Id") + || boost::iequals(it->first, "MUSICBRAINZ_ALBUMID")) { items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, string_trim( string_to_utf8(it->second)) )); } From c295cf501bacc03cd4a1cf5b2be7452b7ed86c6f Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 21 Nov 2015 11:44:11 +0100 Subject: [PATCH 02/29] [AV] Reduce chunk size --- src/ui/resource/AvConvTranscodeStreamResource.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/resource/AvConvTranscodeStreamResource.hpp b/src/ui/resource/AvConvTranscodeStreamResource.hpp index 7ee088c5..b6fbbb94 100644 --- a/src/ui/resource/AvConvTranscodeStreamResource.hpp +++ b/src/ui/resource/AvConvTranscodeStreamResource.hpp @@ -42,7 +42,7 @@ class AvConvTranscodeStreamResource : public Wt::WResource boost::filesystem::path _filePath; Av::TranscodeParameters _parameters; - static const std::size_t _bufferSize = 8192*16; + static const std::size_t _bufferSize = 8192; }; } // namespace UserInterface From 3826377f0180041e71674153d0db828e0ad119b6 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 21 Nov 2015 12:06:01 +0100 Subject: [PATCH 03/29] [DOC] Updated readme to get the proper mobile rendering --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76a47db4..ff89fca0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ $ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --app It is highly recommended to run LMS as a non root user. The exectuable needs write accesses to the /var/lms/ directory. +## Mobile + +Add the following code in your wt_config.xml file: +``` + + true + +``` + ## Setting up SSL materials (optional) Here is just a self signed certificate example, you could do use a CA if you want. @@ -70,4 +79,5 @@ Depending on your SSL parameters, you may be asked for the PEM passphrase to unl - Wt (http://www.webtoolkit.eu/) - bootstrap3 (http://getbootstrap.com/) -- libav project (https://www.libav.org/). +- libav project (https://www.libav.org/) +- Magick++ (http://www.imagemagick.org/Magick++/) From fc88cce33e9d6be9d3eecf59d708f0079f182d23 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 21 Nov 2015 22:11:16 +0100 Subject: [PATCH 04/29] [UI] Refactored some code and integrated CSS changes made by G.Debros --- approot/templates.xml | 76 +++++++++++++ docroot/css/lms.css | 17 +++ src/Makefile.am | 8 +- src/cover/CoverArtGrabber.cpp | 26 +++-- src/database/Release.cpp | 23 ++-- src/database/Release.hpp | 2 - src/database/SearchFilter.hpp | 8 +- src/metadata/AvFormat.cpp | 18 ++- src/metadata/AvFormat.hpp | 2 +- src/ui/audio/desktop/TrackView.cpp | 15 +-- src/ui/audio/mobile/ArtistSearch.cpp | 14 ++- src/ui/audio/mobile/ArtistSearch.hpp | 2 +- src/ui/audio/mobile/MobileAudio.cpp | 31 ++---- src/ui/audio/mobile/ReleaseSearch.cpp | 28 ++++- src/ui/audio/mobile/ReleaseSearch.hpp | 1 + src/ui/audio/mobile/TrackSearch.cpp | 154 +++++++++++++++++--------- src/ui/audio/mobile/TrackSearch.hpp | 14 ++- src/ui/resource/CoverResource.hpp | 1 + src/{metadata => utils}/Utils.cpp | 52 +++++++-- src/{metadata => utils}/Utils.hpp | 53 ++++----- 20 files changed, 369 insertions(+), 176 deletions(-) rename src/{metadata => utils}/Utils.cpp (65%) rename src/{metadata => utils}/Utils.hpp (53%) diff --git a/approot/templates.xml b/approot/templates.xml index 1e9d1148..b0808347 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -444,4 +444,80 @@ + +
+
+
+ ${contents} +
+
+
+
+ + +
+
${cover} +

${name}

${artist}

+
+
+ + +
+
+
+ ${contents} +
+
+
+
+ + +
+
${gif} +

${name}

+
+
+ + + + ${title} + ${release-container} + ${show-more} + + + +
+
+
+
+ ${cover} +
+

${release-name}
${artist-name} - ${year}

+
+ +
+
+ + + + + diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 1bd8c8f4..70e05de6 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -117,3 +117,20 @@ div.contents { height: 48px; } +.release_res_shadow { + 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; +} diff --git a/src/Makefile.am b/src/Makefile.am index ce6a0c12..e8304d03 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,7 +20,6 @@ lms_SOURCES = \ $(srcdir)/image/Image.cpp \ $(srcdir)/logger/Logger.cpp \ $(srcdir)/metadata/AvFormat.cpp \ - $(srcdir)/metadata/Utils.cpp \ $(srcdir)/service/ServiceManager.cpp \ $(srcdir)/service/DatabaseUpdateService.cpp \ $(srcdir)/ui/LmsApplication.cpp \ @@ -34,7 +33,7 @@ lms_SOURCES = \ $(srcdir)/ui/audio/desktop/TrackView.cpp \ $(srcdir)/ui/audio/mobile/ArtistSearch.cpp \ $(srcdir)/ui/audio/mobile/MobileAudio.cpp \ - $(srcdir)/ui/audio/mobile/MobileAudioMediaPlayer.cpp \ + $(srcdir)/ui/audio/mobile/MobileAudioMediaPlayer.cpp \ $(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \ $(srcdir)/ui/audio/mobile/TrackSearch.cpp \ $(srcdir)/ui/common/DirectoryValidator.cpp \ @@ -48,8 +47,9 @@ lms_SOURCES = \ $(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)/ui/settings/SettingsUserFormView.cpp \ + $(srcdir)/ui/settings/SettingsUsers.cpp \ + $(srcdir)/utils/Utils.cpp if VIDEO lms_SOURCES += \ diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 738d04f3..1cfe38ef 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -170,23 +170,25 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re { using namespace Database; - boost::filesystem::path firstTrackPath; - bool embeddedCover = false; + Wt::Dbo::Transaction transaction(session); + + // If the release does not exist or is the special release "None", do nothing + Release::pointer release = Release::getById(session, releaseId); + if (!release || release->isNone()) + return std::vector(); // Get the first track of the release - { - Wt::Dbo::Transaction transaction(session); + std::vector tracks = Track::getByFilter(session, + SearchFilter::ById(SearchFilter::Field::Release, releaseId), + -1, 1 /* limit result size */); - std::vector tracks = Track::getByFilter(session, - SearchFilter::IdMatch({{SearchFilter::Field::Release, {releaseId}}}), - -1, 1 /* limit result size */); + if (tracks.empty()) + return std::vector(); - if (tracks.empty()) - return std::vector(); + boost::filesystem::path firstTrackPath = tracks.front()->getPath(); + bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded); - firstTrackPath = tracks.front()->getPath(); - embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded); - } + transaction.commit(); // First, try to get covers from the directory of the release std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers); diff --git a/src/database/Release.cpp b/src/database/Release.cpp index 5ef5415a..cb683b25 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -44,6 +44,11 @@ Release::getByMBID(Wt::Dbo::Session& session, const std::string& mbid) return session.find().where("mbid = ?").bind(mbid); } +Release::pointer +Release::getById(Wt::Dbo::Session& session, Release::id_type id) +{ + return session.find().where("id = ?").bind(id); +} Release::pointer Release::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID) @@ -51,6 +56,12 @@ Release::create(Wt::Dbo::Session& session, const std::string& name, const std::s return session.add(new Release(name, MBID)); } +bool +Release::isNone() const +{ + return _name == ""; +} + Release::pointer Release::getNone(Wt::Dbo::Session& session) { @@ -129,16 +140,4 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, return std::vector(res.begin(), res.end()); } -std::vector< Wt::Dbo::ptr > -Release::getArtists() const -{ - assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); - assert(session()); - - Wt::Dbo::collection< Wt::Dbo::ptr > res = session()->query >("SELECT a FROM artist a INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.release_id = r.id").where("r.id = ?").bind(id()); - - return std::vector< Wt::Dbo::ptr > (res.begin(), res.end()); -} - } // namespace Database diff --git a/src/database/Release.hpp b/src/database/Release.hpp index 0bb779d0..d9a4d43f 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -65,8 +65,6 @@ class Release : public Wt::Dbo::Dbo std::string getMBID() const { return _MBID; } bool isNone(void) const; boost::posix_time::time_duration getDuration(void) const; - std::vector > getArtists() const; // Get the artists of this release - std::vector > getTracks() const; // Get the tracks of this release void setMBID(std::string mbid) { _MBID = mbid; } diff --git a/src/database/SearchFilter.hpp b/src/database/SearchFilter.hpp index 2d0d397c..00b3f8fc 100644 --- a/src/database/SearchFilter.hpp +++ b/src/database/SearchFilter.hpp @@ -58,12 +58,18 @@ class SearchFilter return SearchFilter(_nameLikeMatch); } - // Single ID match + // Single Field ID match static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id) { return SearchFilter({{field, {id} }}); } + // Single Field Name match + static SearchFilter ByName(Field field, std::vector keywords) + { + return NameLikeMatch({{{field, keywords}}}); + } + // 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 ...) ... diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp index 36bc6c4f..6d751cee 100644 --- a/src/metadata/AvFormat.cpp +++ b/src/metadata/AvFormat.cpp @@ -19,15 +19,12 @@ #include "AvFormat.hpp" -#include +#include "logger/Logger.hpp" +#include "utils/Utils.hpp" #include -#include #include "av/AvInfo.hpp" -#include "logger/Logger.hpp" - -#include "Utils.hpp" namespace MetaData { @@ -112,11 +109,11 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items) for (it = metadata.begin(); it != metadata.end(); ++it) { if (boost::iequals(it->first, "artist")) - items.insert( std::make_pair(MetaData::Type::Artist, string_trim( string_to_utf8(it->second)) )); + items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) )); else if (boost::iequals(it->first, "album")) - items.insert( std::make_pair(MetaData::Type::Album, string_trim( string_to_utf8(it->second)) )); + items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) )); else if (boost::iequals(it->first, "title")) - items.insert( std::make_pair(MetaData::Type::Title, string_trim( string_to_utf8(it->second)) )); + items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) )); else if (boost::iequals(it->first, "track")) { std::size_t number; if (readAs(it->second, number)) @@ -145,6 +142,7 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items) } else if (boost::iequals(it->first, "genre")) { + // TODO use splitStrings std::list genres; if (readList(it->second, ";,", genres)) items.insert( std::make_pair(MetaData::Type::Genres, genres)); @@ -153,12 +151,12 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items) else if (boost::iequals(it->first, "MusicBrainz Artist Id") || boost::iequals(it->first, "MUSICBRAINZ_ARTISTID")) { - items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, string_trim( string_to_utf8(it->second)) )); + items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(it->second)) )); } else if (boost::iequals(it->first, "MusicBrainz Album Id") || boost::iequals(it->first, "MUSICBRAINZ_ALBUMID")) { - items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, string_trim( string_to_utf8(it->second)) )); + items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(it->second)) )); } } diff --git a/src/metadata/AvFormat.hpp b/src/metadata/AvFormat.hpp index da5981b7..4c08d943 100644 --- a/src/metadata/AvFormat.hpp +++ b/src/metadata/AvFormat.hpp @@ -25,7 +25,7 @@ namespace MetaData { -// Implements AVFORMAT library +// Parse that makes use of AvFormat class AvFormat : public Parser { public: diff --git a/src/ui/audio/desktop/TrackView.cpp b/src/ui/audio/desktop/TrackView.cpp index ebf2a432..d01a48ef 100644 --- a/src/ui/audio/desktop/TrackView.cpp +++ b/src/ui/audio/desktop/TrackView.cpp @@ -23,6 +23,7 @@ #include #include "logger/Logger.hpp" +#include "utils/Utils.hpp" #include "LmsApplication.hpp" #include "TrackView.hpp" @@ -41,18 +42,10 @@ class DurationItemDelegate : public Wt::WItemDelegate { boost::posix_time::time_duration duration = boost::any_cast(index.data(Wt::DisplayRole)); - boost::posix_time::time_facet* facet = new boost::posix_time::time_facet(); + // TODO bug when redrawn + std::string format = duration.total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S"; - if (duration.total_seconds() < 3600) - facet->time_duration_format("%M:%S"); - else - facet->time_duration_format("%H:%M:%S"); - - std::ostringstream oss; - oss.imbue(std::locale(oss.getloc(), facet)); - oss << duration; - - return new Wt::WText(oss.str(), Wt::PlainText); + return new Wt::WText(durationToString(duration, format), Wt::PlainText); } }; diff --git a/src/ui/audio/mobile/ArtistSearch.cpp b/src/ui/audio/mobile/ArtistSearch.cpp index f2dc41fb..35033792 100644 --- a/src/ui/audio/mobile/ArtistSearch.cpp +++ b/src/ui/audio/mobile/ArtistSearch.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "LmsApplication.hpp" @@ -38,6 +39,11 @@ _resCount(0) title->setTemplateText(Wt::WString::tr("mobile-search-title")); title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText)); + + Wt::WTemplate* artistWrapper = new Wt::WTemplate(this); + artistWrapper->setTemplateText(Wt::WString::tr("wa-artist-wrapper")); + _contents = new Wt::WContainerWidget(); + artistWrapper->bindWidget("contents", _contents ); } void @@ -75,7 +81,13 @@ ArtistSearch::addResults(Database::SearchFilter filter, std::size_t nb) for (Artist::pointer artist : artists) { Wt::WTemplate* res = new Wt::WTemplate(this); - res->setTemplateText(Wt::WString::tr("mobile-artist-res")); + res->setTemplateText(Wt::WString::tr("wa-artist-res")); + + Wt::WImage *artistImg = new Wt::WImage(); + artistImg->setStyleClass("center-block"); // TODO move in css? + artistImg->setStyleClass("release_res_shadow release_img-responsive"); // TODO move in css? + + res->bindWidget("gif", artistImg); Wt::WText *text = new Wt::WText(Wt::WString::fromUTF8(artist->getName()), Wt::PlainText); res->bindWidget("name", text); diff --git a/src/ui/audio/mobile/ArtistSearch.hpp b/src/ui/audio/mobile/ArtistSearch.hpp index dbe361ca..f1218c9c 100644 --- a/src/ui/audio/mobile/ArtistSearch.hpp +++ b/src/ui/audio/mobile/ArtistSearch.hpp @@ -20,7 +20,6 @@ #ifndef UI_MOBILE_ARTIST_SEARCH_HPP #define UI_MOBILE_ARTIST_SEARCH_HPP -#include #include #include "database/Types.hpp" @@ -48,6 +47,7 @@ class ArtistSearch : public Wt::WContainerWidget void clear(void); void addResults(Database::SearchFilter filter, size_t nb); + Wt::WContainerWidget* _contents; std::size_t _resCount; }; diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp index 5d910dd8..67a2b995 100644 --- a/src/ui/audio/mobile/MobileAudio.cpp +++ b/src/ui/audio/mobile/MobileAudio.cpp @@ -17,9 +17,6 @@ * along with LMS. If not, see . */ -#include -#include -#include #include #include @@ -27,16 +24,17 @@ #include #include -#include "LmsApplication.hpp" +#include "logger/Logger.hpp" +#include "utils/Utils.hpp" -#include "MobileAudio.hpp" +#include "LmsApplication.hpp" #include "ArtistSearch.hpp" #include "ReleaseSearch.hpp" #include "TrackSearch.hpp" #include "MobileAudioMediaPlayer.hpp" -#include "logger/Logger.hpp" +#include "MobileAudio.hpp" namespace UserInterface { namespace Mobile { @@ -87,26 +85,17 @@ Audio::Audio(Wt::WContainerWidget *parent) AudioMediaPlayer* mediaPlayer = new AudioMediaPlayer(encoding); footer->bindWidget("player", mediaPlayer); - edit->changed().connect(std::bind([=] () { - std::string text = edit->text().toUTF8(); - + edit->changed().connect(std::bind([=] () + { // When a new search is done, output some results from: // Artist // Release // Song - std::vector keywords; - boost::algorithm::split(keywords, text, boost::is_any_of(" "), boost::token_compress_on); + std::vector keywords = splitStrings(edit->text().toUTF8(), " ");; - releaseSearch->search(SearchFilter::NameLikeMatch( {{ - { SearchFilter::Field::Artist, keywords }, - { SearchFilter::Field::Release, keywords }}}), - 3); - - artistSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Artist, keywords}}}), - 3); - - trackSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Track, keywords}}}), - 3); + releaseSearch->search(SearchFilter::ByName(SearchFilter::Field::Release, keywords), 4); + artistSearch->search(SearchFilter::ByName(SearchFilter::Field::Artist, keywords), 4); + trackSearch->search(SearchFilter::ByName(SearchFilter::Field::Track, keywords), 4); artistSearch->show(); releaseSearch->show(); diff --git a/src/ui/audio/mobile/ReleaseSearch.cpp b/src/ui/audio/mobile/ReleaseSearch.cpp index d9af0cab..fc9092e1 100644 --- a/src/ui/audio/mobile/ReleaseSearch.cpp +++ b/src/ui/audio/mobile/ReleaseSearch.cpp @@ -40,6 +40,11 @@ _resCount(0) title->setTemplateText(Wt::WString::tr("mobile-search-title")); title->bindWidget("text", new Wt::WText("Releases", Wt::PlainText)); + + Wt::WTemplate* releaseWrapper = new Wt::WTemplate(this); + releaseWrapper->setTemplateText(Wt::WString::tr("wa-release-wrapper")); + _contents = new Wt::WContainerWidget(); + releaseWrapper->bindWidget("contents", _contents ); } void @@ -58,9 +63,23 @@ ReleaseSearch::search(Database::SearchFilter filter, size_t max) addResults(filter, max); } +static Wt::WString +getArtistFromRelease(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 + return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText); +} + void ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb) { + using namespace Database; + Wt::Dbo::Transaction transaction(DboSession()); std::vector releases = Release::getByFilter(DboSession(), filter, _resCount, nb + 1); @@ -77,14 +96,17 @@ ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb) for (Release::pointer release : releases) { Wt::WTemplate* releaseWidget = new Wt::WTemplate(this); - releaseWidget->setTemplateText(Wt::WString::tr("mobile-release-res")); + releaseWidget->setTemplateText(Wt::WString::tr("wa-release-res")); Wt::WImage *cover = new Wt::WImage(); cover->setStyleClass("center-block"); - cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 56))); - releaseWidget->bindWidget("cover", cover); + cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512))); + cover->setStyleClass("release_res_shadow release_img-responsive"); // TODO move? + releaseWidget->bindWidget("cover", cover); releaseWidget->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText)); + releaseWidget->bindString("release_name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText); + releaseWidget->bindString("artist", getArtistFromRelease(release)); releaseWidget->clicked().connect(std::bind([=] { _sigReleaseSelected(release.id()); diff --git a/src/ui/audio/mobile/ReleaseSearch.hpp b/src/ui/audio/mobile/ReleaseSearch.hpp index 876c1492..7cbd0058 100644 --- a/src/ui/audio/mobile/ReleaseSearch.hpp +++ b/src/ui/audio/mobile/ReleaseSearch.hpp @@ -48,6 +48,7 @@ class ReleaseSearch : public Wt::WContainerWidget void clear(void); void addResults(Database::SearchFilter filter, size_t nb); + Wt::WContainerWidget* _contents; std::size_t _resCount; }; diff --git a/src/ui/audio/mobile/TrackSearch.cpp b/src/ui/audio/mobile/TrackSearch.cpp index 0c1b9b94..10e97742 100644 --- a/src/ui/audio/mobile/TrackSearch.cpp +++ b/src/ui/audio/mobile/TrackSearch.cpp @@ -23,6 +23,7 @@ #include #include "logger/Logger.hpp" +#include "utils/Utils.hpp" #include "LmsApplication.hpp" #include "TrackSearch.hpp" @@ -33,101 +34,146 @@ namespace Mobile { using namespace Database; TrackSearch::TrackSearch(Wt::WContainerWidget *parent) -: Wt::WContainerWidget(parent), -_resCount(0) +: Wt::WContainerWidget(parent) { + 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); - title->bindWidget("text", new Wt::WText("Tracks", 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(); } void TrackSearch::clear() { - while (count() > 1) - removeWidget(this->widget(1)); + // Flush the release container + _releaseContainer->clear(); - _resCount = 0; + // Flush the current context + _currentTrackContainer = nullptr; + _nbTracks = 0; + _showMore->hide(); } void -TrackSearch::search(Database::SearchFilter filter, size_t max) +TrackSearch::search(SearchFilter filter, size_t nb) { + _filter = filter; + clear(); - addResults(filter, max); + addResults(nb); } -void -TrackSearch::addResults(Database::SearchFilter filter, size_t nb) +static +std::vector +getTracks(SearchFilter filter, size_t offset, size_t nb, bool &moreResults) { - Wt::Dbo::Transaction transaction(DboSession()); + std::vector tracks = Track::getByFilter(DboSession(), filter, offset, nb + 1); - std::vector tracks = Track::getByFilter(DboSession(), filter, _resCount, nb + 1); - - bool expectMoreResults; if (tracks.size() == nb + 1) { - expectMoreResults = true; + moreResults = true; tracks.pop_back(); } else - expectMoreResults = false; + moreResults = false; + + return tracks; +} + +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 + return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText); +} + +void +TrackSearch::addResults(size_t nb) +{ + Wt::Dbo::Transaction transaction(DboSession()); + + bool moreResults; + std::vector tracks = getTracks(_filter, _nbTracks, nb, moreResults); for (Track::pointer track : tracks) { - Wt::WTemplate* trackWidget = new Wt::WTemplate(this); - trackWidget->setTemplateText(Wt::WString::tr("mobile-track-res")); + // First check if we need to create a new track container - Wt::WImage *cover = new Wt::WImage(); - cover->setStyleClass("center-block"); - cover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 56)) ); - trackWidget->bindWidget("cover", cover); - - // Track Name (bold) - // Artist - Album (italic) - Wt::WContainerWidget *container = new Wt::WContainerWidget(); - Wt::WText *title = new Wt::WText(Wt::WString::fromUTF8(track->getName()), Wt::PlainText); - title->setStyleClass("mobile-track"); - container->addWidget(title); - - if (!track->getArtist()->getName().empty() - || !track->getRelease()->getName().empty()) + // New container if it is the first one or if the released has changed + if (!_currentTrackContainer + || _currentReleaseId != track->getRelease().id()) { - title->setInline(false); - Wt::WText *artistRelease = new Wt::WText(Wt::WString::fromUTF8(track->getArtist()->getName() + " - " + track->getRelease()->getName()), Wt::PlainText); - artistRelease->setStyleClass("mobile-artist"); - container->addWidget(artistRelease); - } - trackWidget->bindWidget("name", container); + Release::pointer release = track->getRelease(); - Wt::WPushButton *playBtn = new Wt::WPushButton("Play"); - playBtn->setStyleClass("btn-primary center-block"); + 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 (LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512))); + + releaseContainer->bindWidget("cover", cover); + releaseContainer->bindString("artist-name", getArtistNameFromRelease(release)); + releaseContainer->bindString("release-name", release->getName(), Wt::PlainText); + releaseContainer->bindInt("year", 2001); + + _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")); + + // TODO, do not display track pos for tracks that are not part of a release + trackRes->bindInt("track-pos", track->getTrackNumber()); + trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText); + // TODO, display artist name for compilation releases? + + // TODO handle large duration (> 1 hour) + trackRes->bindString("time", durationToString(track->getDuration(), "%M:%S"), Wt::PlainText); + + Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText); + playBtn->setStyleClass("center-block"); // TODO move to CSS? playBtn->clicked().connect(std::bind([=] { _sigTrackPlay.emit(track.id()); })); - trackWidget->bindWidget("btn", playBtn); - + trackRes->bindWidget("btn", playBtn); + _nbTracks++; } - _resCount += tracks.size(); - - if (expectMoreResults) + if (moreResults) { - Wt::WTemplate* moreRes = new Wt::WTemplate(this); - moreRes->setTemplateText(Wt::WString::tr("mobile-search-more")); - - moreRes->bindWidget("text", new Wt::WText("Tap to show more results...")); - - moreRes->clicked().connect(std::bind([=] { + _showMore->clicked().connect(std::bind([=] { _sigMoreTracksSelected(); - removeWidget(moreRes); - - addResults(filter, 20); + addResults(20); })); + _showMore->show(); } + else + _showMore->hide(); } } // namespace Mobile diff --git a/src/ui/audio/mobile/TrackSearch.hpp b/src/ui/audio/mobile/TrackSearch.hpp index 6cf3b317..adc80189 100644 --- a/src/ui/audio/mobile/TrackSearch.hpp +++ b/src/ui/audio/mobile/TrackSearch.hpp @@ -48,9 +48,19 @@ class TrackSearch : public Wt::WContainerWidget Wt::Signal _sigMoreTracksSelected; void clear(void); - void addResults(Database::SearchFilter filter, size_t nb); + void addResults(size_t nb); - std::size_t _resCount; + 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 diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index 82fad5d4..abd14cd0 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -41,6 +41,7 @@ class CoverResource : public Wt::WResource std::string getReleaseUrl(Database::Release::id_type releaseId, size_t size) const; std::string getTrackUrl(Database::Track::id_type trackId, size_t size) const; + std::string getArtistUrl(Database::Artist::id_type artistId, size_t size) const; std::string getUnknownTrackUrl(size_t size) const; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); diff --git a/src/metadata/Utils.cpp b/src/utils/Utils.cpp similarity index 65% rename from src/metadata/Utils.cpp rename to src/utils/Utils.cpp index ff926363..e95ea8d0 100644 --- a/src/metadata/Utils.cpp +++ b/src/utils/Utils.cpp @@ -21,12 +21,11 @@ #include #include -#include +#include +#include #include "Utils.hpp" -namespace MetaData -{ bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time) { @@ -56,11 +55,11 @@ bool readList(const std::string& str, const std::string& separators, std::listtime_duration_format(format.c_str()); + std::ostringstream oss; + oss.imbue(std::locale(oss.getloc(), facet)); + oss << duration; + return oss.str(); } +std::vector +splitStrings(std::string string, std::string separators) +{ + std::vector res; + + boost::algorithm::split(res, string, boost::is_any_of(separators), boost::token_compress_on); + + return res; +} + +std::string +stringTrim(const std::string& str, const std::string& whitespace) +{ + const auto strBegin = str.find_first_not_of(whitespace); + if (strBegin == std::string::npos) + return ""; // no content + + const auto strEnd = str.find_last_not_of(whitespace); + const auto strRange = strEnd - strBegin + 1; + + return str.substr(strBegin, strRange); +} + +std::string +stringToUTF8(const std::string& str) +{ + return boost::locale::conv::to_utf(str, "UTF-8"); +} + + diff --git a/src/metadata/Utils.hpp b/src/utils/Utils.hpp similarity index 53% rename from src/metadata/Utils.hpp rename to src/utils/Utils.hpp index 786e1a2b..f59b840c 100644 --- a/src/metadata/Utils.hpp +++ b/src/utils/Utils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Emeric Poupon + * Copyright (C) 2015 Emeric Poupon * * This file is part of LMS. * @@ -17,20 +17,32 @@ * along with LMS. If not, see . */ -#ifndef METADATA_UTILS_HPP -#define METADATA_UTILS_HPP +#pragma once #include +#include #include #include +#include -namespace MetaData -{ +bool +readAsPosixTime(const std::string& str, boost::posix_time::ptime& time); -bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time); +bool +readList(const std::string& str, const std::string& separators, std::list& results); -bool readList(const std::string& str, const std::string& separators, std::list& results); +std::string +durationToString(boost::posix_time::time_duration duration, std::string format); + +std::vector +splitStrings(std::string string, std::string separators); + +std::string +stringTrim(const std::string& str, const std::string& whitespace = " \t"); + +std::string +stringToUTF8(const std::string& str); template static inline bool readAs(const std::string& str, T& data) @@ -40,30 +52,3 @@ static inline bool readAs(const std::string& str, T& data) return !iss.fail(); } - - -std::string -static inline string_trim(const std::string& str, - const std::string& whitespace = " \t") -{ - const auto strBegin = str.find_first_not_of(whitespace); - if (strBegin == std::string::npos) - return ""; // no content - - const auto strEnd = str.find_last_not_of(whitespace); - const auto strRange = strEnd - strBegin + 1; - - return str.substr(strBegin, strRange); -} - - -std::string -static inline string_to_utf8(const std::string& str) -{ - return boost::locale::conv::to_utf(str, "UTF-8"); -} - -} // namespace MetaData - -#endif - From 963e5911e3e6b5ec7b471a7dcb015e56b3ff06c1 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 22 Nov 2015 15:01:00 +0100 Subject: [PATCH 05/29] [UI/MOBILE] Handling year in release --- src/database/Release.cpp | 24 ++++++++++++++++++++++++ src/database/Release.hpp | 3 +++ src/ui/audio/mobile/MobileAudio.cpp | 14 ++++++++------ src/ui/audio/mobile/TrackSearch.cpp | 14 ++++++-------- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/database/Release.cpp b/src/database/Release.cpp index cb683b25..ba4f88d2 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -140,4 +140,28 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, return std::vector(res.begin(), res.end()); } +int +Release::getReleaseYear(bool original) const +{ + assert(session()); + + // TODO something better + auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1); + + if (tracks.empty()) + return 0; + + boost::gregorian::date date; + + if (original) + date = tracks.front()->getOriginalDate().date(); + else + date = tracks.front()->getDate().date(); + + if (date.is_special()) + return 0; + + return date.year(); +} + } // namespace Database diff --git a/src/database/Release.hpp b/src/database/Release.hpp index d9a4d43f..2ace3867 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -53,6 +53,9 @@ class Release : public Wt::Dbo::Dbo // Create static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = ""); + // Utility functions + int getReleaseYear(bool originalDate = false) const; // 0 if unknown or various + // MVC models for the user interface // ID, Release name, year, track counts typedef boost::tuple UIQueryResult; diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp index 67a2b995..054710dd 100644 --- a/src/ui/audio/mobile/MobileAudio.cpp +++ b/src/ui/audio/mobile/MobileAudio.cpp @@ -36,6 +36,8 @@ #include "MobileAudio.hpp" +#define SEARCH_NB_ITEMS 4 + namespace UserInterface { namespace Mobile { @@ -93,9 +95,9 @@ Audio::Audio(Wt::WContainerWidget *parent) // Song std::vector keywords = splitStrings(edit->text().toUTF8(), " ");; - releaseSearch->search(SearchFilter::ByName(SearchFilter::Field::Release, keywords), 4); - artistSearch->search(SearchFilter::ByName(SearchFilter::Field::Artist, keywords), 4); - trackSearch->search(SearchFilter::ByName(SearchFilter::Field::Track, keywords), 4); + releaseSearch->search(SearchFilter::ByName(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS); + artistSearch->search(SearchFilter::ByName(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS); + trackSearch->search(SearchFilter::ByName(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS); artistSearch->show(); releaseSearch->show(); @@ -175,9 +177,9 @@ Audio::Audio(Wt::WContainerWidget *parent) // Initially, populate the widgets using an empty search { - artistSearch->search(SearchFilter(), 3); - releaseSearch->search(SearchFilter(), 3); - trackSearch->search(SearchFilter(), 3); + artistSearch->search(SearchFilter(), SEARCH_NB_ITEMS); + releaseSearch->search(SearchFilter(), SEARCH_NB_ITEMS); + trackSearch->search(SearchFilter(), SEARCH_NB_ITEMS); } } diff --git a/src/ui/audio/mobile/TrackSearch.cpp b/src/ui/audio/mobile/TrackSearch.cpp index 10e97742..cad8ca43 100644 --- a/src/ui/audio/mobile/TrackSearch.cpp +++ b/src/ui/audio/mobile/TrackSearch.cpp @@ -54,6 +54,10 @@ TrackSearch::TrackSearch(Wt::WContainerWidget *parent) _showMore->setTemplateText(Wt::WString::tr("mobile-search-more")); _showMore->bindString("text", "Tap to show more results..."); _showMore->hide(); + _showMore->clicked().connect(std::bind([=] { + _sigMoreTracksSelected(); + addResults(20); + })); } void @@ -133,9 +137,9 @@ TrackSearch::addResults(size_t nb) cover->setImageLink(Wt::WLink (LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512))); releaseContainer->bindWidget("cover", cover); - releaseContainer->bindString("artist-name", getArtistNameFromRelease(release)); + releaseContainer->bindString("artist-name", getArtistNameFromRelease(release), Wt::PlainText); releaseContainer->bindString("release-name", release->getName(), Wt::PlainText); - releaseContainer->bindInt("year", 2001); + releaseContainer->bindInt("year", release->getReleaseYear()); _currentTrackContainer = new Wt::WContainerWidget(); releaseContainer->bindWidget("track-container", _currentTrackContainer); @@ -165,13 +169,7 @@ TrackSearch::addResults(size_t nb) } if (moreResults) - { - _showMore->clicked().connect(std::bind([=] { - _sigMoreTracksSelected(); - addResults(20); - })); _showMore->show(); - } else _showMore->hide(); } From fd6627051dac1c972298f98a8866f88669350f37 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 22 Nov 2015 15:33:27 +0100 Subject: [PATCH 06/29] [UI/MOBILE] Condition year, original year, track num and disc num --- approot/templates.xml | 4 ++-- src/ui/audio/mobile/TrackSearch.cpp | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/approot/templates.xml b/approot/templates.xml index b0808347..e930225d 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -505,7 +505,7 @@
${cover}
-

${release-name}
${artist-name} - ${year}

+

${release-name}
${artist-name}${} - ${year}${}${} (${orig-year})${}

+ + ${title} + + ${show-more} + + + + + + + + +
+ ${collapse-button} + + ${contents} +
+
+ ${title}
diff --git a/src/logger/Logger.hpp b/src/logger/Logger.hpp index 1f08c04a..b3b7b7a4 100644 --- a/src/logger/Logger.hpp +++ b/src/logger/Logger.hpp @@ -17,8 +17,7 @@ * along with LMS. If not, see . */ -#ifndef LOGGER_HPP__ -#define LOGGER_HPP__ +#pragma once #include #include @@ -55,4 +54,3 @@ std::string getSeverityName(Severity sev); #define LMS_LOG(module, level) Wt::log(getSeverityName(Severity::level)) << Wt::WLogger::sep << "[" << getModuleName(Module::module) << "]" << Wt::WLogger::sep -#endif diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index cdf5c7e7..10be2979 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -204,9 +203,15 @@ LmsApplication::handleAuthEvent(void) contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto); contentsStack->addStyleClass("contents"); - // Setup a Left-aligned menu. - Wt::WMenu *leftMenu = new Wt::WMenu(contentsStack); - navigation->addMenu(leftMenu); + Wt::WMenu *menu = new Wt::WMenu(contentsStack); + navigation->addMenu(menu, Wt::AlignRight); + + Wt::WLineEdit *searchEdit = new Wt::WLineEdit(); + navigation->bindWidget("search", searchEdit); + searchEdit->setEmptyText("Search..."); + searchEdit->addStyleClass("navbar-form navbar-nav"); + searchEdit->setWidth(150); + // TODO add a span with a search icon Audio *audio; @@ -215,23 +220,18 @@ LmsApplication::handleAuthEvent(void) else audio = new Desktop::Audio(); - leftMenu->addItem("Audio", audio); + menu->addItem("Audio", audio); #if defined HAVE_VIDEO - leftMenu->addItem("Video", new VideoWidget()); + menu->addItem("Video", new VideoWidget()); #endif - leftMenu->addItem("Settings", new Settings::Settings()); - - // Setup a Right-aligned menu. - Wt::WMenu *rightMenu = new Wt::WMenu(); - - navigation->addMenu(rightMenu, Wt::AlignRight); + menu->addItem("Settings", new Settings::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); - rightMenu->addItem(item); + item->setMenu(popup); + menu->addItem(item); popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item) { @@ -239,17 +239,12 @@ LmsApplication::handleAuthEvent(void) DbHandler().getLogin().logout(); }, std::placeholders::_1)); - // Add a Search control. - LineEdit *searchEdit = new LineEdit(500); - searchEdit->setEmptyText("Search..."); - - searchEdit->timedChanged().connect(std::bind([=] () + searchEdit->changed().connect(std::bind([=] () { // TODO: check which view is activated and search into it audio->search(searchEdit->text().toUTF8()); })); - navigation->addSearch(searchEdit, Wt::AlignLeft); layout->addWidget(navigation); layout->addWidget(contentsStack, 1); diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index 9b3d6196..f529dc78 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -25,6 +25,7 @@ #include #include "common/InputRange.hpp" +#include "logger/Logger.hpp" #include "LmsApplication.hpp" @@ -40,7 +41,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); if (!track) { - std::cerr << "no track for this id!" << std::endl; + LMS_LOG(UI, INFO) << "No track found for id " << trackId; return; } @@ -55,7 +56,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) if (!mediaFile.open() || !mediaFile.scan()) { - std::cerr << "cannot open file '" << track->getPath() << std::endl; + LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath(); return; } @@ -90,6 +91,8 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) t->setTemplateText(Wt::WString::tr("wa-audio-player")); _audio = new Wt::WAudio(this); + _audio->setOptions(Wt::WAudio::Autoplay); + _audio->setPreloadMode(Wt::WAudio::PreloadAuto); _cover = new Wt::WImage(); t->bindWidget("cover", _cover); diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp index 2a4eb065..b5d0c66a 100644 --- a/src/ui/audio/mobile/MobileAudio.cpp +++ b/src/ui/audio/mobile/MobileAudio.cpp @@ -20,9 +20,7 @@ #include #include -#include #include -#include #include "logger/Logger.hpp" #include "utils/Utils.hpp" @@ -30,11 +28,6 @@ #include "audio/AudioPlayer.hpp" #include "LmsApplication.hpp" -#include "ArtistSearch.hpp" -#include "ReleaseSearch.hpp" -#include "TrackSearch.hpp" -#include "TrackReleaseView.hpp" - #include "MobileAudio.hpp" #define SEARCH_NB_ITEMS 4 @@ -56,6 +49,26 @@ static void playTrack(AudioPlayer *audioPlayer, Database::Track::id_type trackId audioPlayer->loadTrack(track.id()); } +void +Audio::search(std::string text) +{ + // When a new search is done, output some results from: + // Artist + // Release + // Song + auto keywords = splitString(text, " ");; + + _releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS); + _artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS); + _trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS); + + _artistSearch->show(); + _releaseSearch->show(); + _trackSearch->show(); + _trackReleaseView->hide(); +} + + Audio::Audio(Wt::WContainerWidget *parent) : UserInterface::Audio(parent) { @@ -63,21 +76,12 @@ Audio::Audio(Wt::WContainerWidget *parent) this->setStyleClass("container-fluid"); this->setPadding(60, Wt::Bottom); - Wt::WTemplate* search = new Wt::WTemplate(this); - search->setTemplateText(Wt::WString::tr("mobile-search")); + _artistSearch = new ArtistSearch(this); + _releaseSearch = new ReleaseSearch(this); + _trackSearch = new TrackSearch(this); - Wt::WLineEdit *edit = new Wt::WLineEdit(); - edit->setEmptyText("Search..."); - - search->bindWidget("search", edit); - search->setMargin(10); - - ArtistSearch* artistSearch = new ArtistSearch(this); - ReleaseSearch* releaseSearch = new ReleaseSearch(this); - TrackSearch* trackSearch = new TrackSearch(this); - - TrackReleaseView* trackReleaseView = new TrackReleaseView(this); - trackReleaseView->hide(); + _trackReleaseView = new TrackReleaseView(this); + _trackReleaseView->hide(); Wt::WTemplate* footer = new Wt::WTemplate(this); footer->setTemplateText(Wt::WString::tr("mobile-audio-footer")); @@ -85,83 +89,65 @@ Audio::Audio(Wt::WContainerWidget *parent) AudioPlayer* audioPlayer = new AudioPlayer(); footer->bindWidget("player", audioPlayer); - edit->changed().connect(std::bind([=] () + _artistSearch->moreArtistsSelected().connect(std::bind([=] { - // When a new search is done, output some results from: - // Artist - // Release - // Song - auto keywords = splitString(edit->text().toUTF8(), " ");; - - releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS); - artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS); - trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS); - - artistSearch->show(); - releaseSearch->show(); - trackSearch->show(); - trackReleaseView->hide(); + _releaseSearch->hide(); + _trackSearch->hide(); + _artistSearch->show(); + _trackReleaseView->hide(); })); - artistSearch->moreArtistsSelected().connect(std::bind([=] + _artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId) { - releaseSearch->hide(); - trackSearch->hide(); - artistSearch->show(); - trackReleaseView->hide(); - })); + _artistSearch->hide(); + _trackSearch->hide(); + _releaseSearch->show(); + _trackReleaseView->hide(); - artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId) - { - artistSearch->hide(); - trackSearch->hide(); - releaseSearch->show(); - trackReleaseView->hide(); - - releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20); + _releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20); }, std::placeholders::_1)); - releaseSearch->moreReleasesSelected().connect(std::bind([=] + _releaseSearch->moreReleasesSelected().connect(std::bind([=] { - artistSearch->hide(); - trackSearch->hide(); - releaseSearch->show(); - trackReleaseView->hide(); + _artistSearch->hide(); + _trackSearch->hide(); + _releaseSearch->show(); + _trackReleaseView->hide(); })); - releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId) + _releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId) { - artistSearch->hide(); - releaseSearch->hide(); - trackSearch->hide(); - trackReleaseView->show(); + _artistSearch->hide(); + _releaseSearch->hide(); + _trackSearch->hide(); + _trackReleaseView->show(); - trackReleaseView->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 40); + _trackReleaseView->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 40); }, std::placeholders::_1)); - trackSearch->moreSelected().connect(std::bind([=] + _trackSearch->moreSelected().connect(std::bind([=] { - artistSearch->hide(); - releaseSearch->hide(); - trackSearch->show(); - trackReleaseView->hide(); + _artistSearch->hide(); + _releaseSearch->hide(); + _trackSearch->show(); + _trackReleaseView->hide(); })); - trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id) + _trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id) { playTrack(audioPlayer, id); }, std::placeholders::_1)); - trackReleaseView->trackPlay().connect(std::bind([=] (Track::id_type id) + _trackReleaseView->trackPlay().connect(std::bind([=] (Track::id_type id) { playTrack(audioPlayer, id); }, std::placeholders::_1)); // Initially, populate the widgets using an empty search { - artistSearch->search(SearchFilter(), SEARCH_NB_ITEMS); - releaseSearch->search(SearchFilter(), SEARCH_NB_ITEMS); - trackSearch->search(SearchFilter(), SEARCH_NB_ITEMS); + _artistSearch->search(SearchFilter(), SEARCH_NB_ITEMS); + _releaseSearch->search(SearchFilter(), SEARCH_NB_ITEMS); + _trackSearch->search(SearchFilter(), SEARCH_NB_ITEMS); } } diff --git a/src/ui/audio/mobile/MobileAudio.hpp b/src/ui/audio/mobile/MobileAudio.hpp index e10a3a57..a62e9951 100644 --- a/src/ui/audio/mobile/MobileAudio.hpp +++ b/src/ui/audio/mobile/MobileAudio.hpp @@ -24,6 +24,11 @@ #include "audio/Audio.hpp" +#include "ArtistSearch.hpp" +#include "ReleaseSearch.hpp" +#include "TrackSearch.hpp" +#include "TrackReleaseView.hpp" + namespace UserInterface { namespace Mobile { @@ -32,10 +37,14 @@ class Audio : public UserInterface::Audio public: Audio(Wt::WContainerWidget *parent = 0); - void search(std::string text) {} + void search(std::string text); private: + ArtistSearch* _artistSearch; + ReleaseSearch* _releaseSearch; + TrackSearch* _trackSearch; + TrackReleaseView* _trackReleaseView; }; } // namespace Mobile From c24dd3c9d85ccaacfb90e1371fd46fade19ad834 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 5 Jan 2016 21:51:12 +0100 Subject: [PATCH 17/29] [UI/MOBILE] Restored the old timed linedit search --- TODO | 1 + src/ui/LmsApplication.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 9f28f3fb..fbffd99b 100644 --- a/TODO +++ b/TODO @@ -50,6 +50,7 @@ - drop the winamp like interface? - Add a download button to get the current playlist in a streamed zip file - Add a upload button to upload media files in a dedicated directory + - Perform a search for each key pressed (using a timer, in JS) [desktop] - Better style... - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 10be2979..be39ef1f 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -206,7 +206,7 @@ LmsApplication::handleAuthEvent(void) Wt::WMenu *menu = new Wt::WMenu(contentsStack); navigation->addMenu(menu, Wt::AlignRight); - Wt::WLineEdit *searchEdit = new Wt::WLineEdit(); + LineEdit *searchEdit = new LineEdit(500); navigation->bindWidget("search", searchEdit); searchEdit->setEmptyText("Search..."); searchEdit->addStyleClass("navbar-form navbar-nav"); @@ -239,7 +239,7 @@ LmsApplication::handleAuthEvent(void) DbHandler().getLogin().logout(); }, std::placeholders::_1)); - searchEdit->changed().connect(std::bind([=] () + searchEdit->timedChanged().connect(std::bind([=] () { // TODO: check which view is activated and search into it audio->search(searchEdit->text().toUTF8()); From e2dee6d4b86468dd3f06d3af92c7eb35d3b2626c Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 11 Jan 2016 20:24:41 +0100 Subject: [PATCH 18/29] [UI] Updated audio player layout --- approot/templates.xml | 2 +- docroot/css/lms.css | 39 +-- src/Makefile.am | 1 - src/ui/audio/AudioPlayer.cpp | 204 +++++++++------ src/ui/audio/AudioPlayer.hpp | 21 +- src/ui/audio/desktop/AudioMediaPlayer.cpp | 289 ---------------------- src/ui/audio/desktop/AudioMediaPlayer.hpp | 110 -------- src/ui/audio/desktop/DesktopAudio.cpp | 121 +++++---- src/ui/audio/desktop/DesktopAudio.hpp | 9 +- 9 files changed, 232 insertions(+), 564 deletions(-) delete mode 100644 src/ui/audio/desktop/AudioMediaPlayer.cpp delete mode 100644 src/ui/audio/desktop/AudioMediaPlayer.hpp diff --git a/approot/templates.xml b/approot/templates.xml index b7924f8d..1f8f11a6 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -529,7 +529,7 @@ - ${shuffle} ${repeat} ${playlist} ${prev} ${play-pause} ${next} ${cover} ${artist} ${track} ${release} ${curtime} ${seekbar} ${duration} ${volume} + ${audio}${prev}${play-pause}${next}${cover}${artist}${track}${release}${curtime}${seekbar}${duration}${volume}${shuffle}${repeat}${playlist} diff --git a/docroot/css/lms.css b/docroot/css/lms.css index afe63087..304be0ac 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -10,11 +10,6 @@ background-color: white; } -div.contents { - padding: 0px 12px 6px; - margin-bottom: 64px; -} - .playqueue { background-color: #EEE; border-radius: 10px; @@ -40,15 +35,6 @@ div.contents { height: 64px; } -.mediaplayer { - background-color: #CCC; - border-radius: 10px; - min-width: 360px; -} - -.mediaplayer-btn-controls { - font-weight: bold; -} .mediaplayer-current-cover { width: 72px; @@ -57,15 +43,36 @@ div.contents { box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); } -.mediaplayer-current-track { +.mediaplayer-track { font-weight: bold; font-size: 120%; } -.mediaplayer-current-artist { +.mediaplayer-artist { font-style: italic; } +.mediaplayer { + background-color: #CCC; + border-radius: 10px; + display: flex; + justify-content: space-between; + align-items: center; + height: 72px; +} + +.mediaplayer-btn { + margin: 8px; +} + +.mediaplayer-btn:before { + cursor: pointer; +} + +.mediaplayer-btn-active { + color: #428bca; +} + .mobile-search-title { font-weight: bold; height: 32px; diff --git a/src/Makefile.am b/src/Makefile.am index 7cc1077c..e6b0f919 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,7 +26,6 @@ lms_SOURCES = \ $(srcdir)/ui/auth/LmsAuth.cpp \ $(srcdir)/ui/audio/AudioPlayer.cpp \ $(srcdir)/ui/audio/desktop/DesktopAudio.cpp \ - $(srcdir)/ui/audio/desktop/AudioMediaPlayer.cpp \ $(srcdir)/ui/audio/desktop/FilterChain.cpp \ $(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \ $(srcdir)/ui/audio/desktop/PlayQueue.cpp \ diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index f529dc78..be5b68c3 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -33,7 +33,7 @@ namespace UserInterface { -void +bool AudioPlayer::loadTrack(Database::Track::id_type trackId) { Wt::Dbo::Transaction transaction(DboSession()); @@ -42,7 +42,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) if (!track) { LMS_LOG(UI, INFO) << "No track found for id " << trackId; - return; + return false; } _trackName->setText(Wt::WString::fromUTF8(track->getName())); @@ -57,7 +57,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) if (!mediaFile.open() || !mediaFile.scan()) { LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath(); - return; + return false; } @@ -79,120 +79,170 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) //TODO, try to load everything in JS in order to prevent the WriteError bug? _audio->pause(); _audio->clearSources(); - //TODOencoding + //TODO, encoding _audio->addSource(SessionTranscodeResource()->getUrl(trackId, Av::Encoding::MP3, 0, streams)); + _audio->setPreloadMode(Wt::WAudio::PreloadAuto); _audio->play(); + + return true; } AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) -: Wt::WContainerWidget(parent) +: Wt::WTemplate(parent) { - Wt::WTemplate *t = new Wt::WTemplate(this); - t->setTemplateText(Wt::WString::tr("wa-audio-player")); + setTemplateText(Wt::WString::tr("wa-audio-player")); + addStyleClass("mediaplayer"); - _audio = new Wt::WAudio(this); + // TODO potential leak here + _audio = new Wt::WAudio(); _audio->setOptions(Wt::WAudio::Autoplay); _audio->setPreloadMode(Wt::WAudio::PreloadAuto); + bindWidget("audio", _audio); + + _audio->ended().connect(std::bind([=] () + { + _playbackEnded.emit(); + })); _cover = new Wt::WImage(); - t->bindWidget("cover", _cover); + bindWidget("cover", _cover); _cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64)); InputRange *seekbar = new InputRange(); - t->bindWidget("seekbar", seekbar); + bindWidget("seekbar", seekbar); _trackName = new Wt::WText(); - t->bindWidget("track", _trackName); + bindWidget("track", _trackName); _artistName = new Wt::WText(); - t->bindWidget("artist", _artistName); + bindWidget("artist", _artistName); _releaseName = new Wt::WText(); - t->bindWidget("release", _releaseName); + bindWidget("release", _releaseName); InputRange *volumeSlider = new InputRange(); - t->bindWidget("volume", volumeSlider); + bindWidget("volume", volumeSlider); - Wt::WPushButton *playlistBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("playlist", playlistBtn); + Wt::WText *playlistBtn = new Wt::WText("", Wt::XHTMLText); + playlistBtn->addStyleClass("mediaplayer-btn"); + bindWidget("playlist", playlistBtn); - Wt::WPushButton *repeatBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("repeat", repeatBtn); + Wt::WText *repeatBtn = new Wt::WText("", Wt::XHTMLText); + repeatBtn->addStyleClass("mediaplayer-btn"); + 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); + } + })); - Wt::WPushButton *shuffleBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("shuffle", shuffleBtn); + Wt::WText *shuffleBtn = new Wt::WText("", Wt::XHTMLText); + shuffleBtn->addStyleClass("mediaplayer-btn"); + 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::WPushButton *prevBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("prev", prevBtn); + Wt::WText *prevBtn = new Wt::WText("", Wt::XHTMLText); + prevBtn->addStyleClass("mediaplayer-btn"); + bindWidget("prev", prevBtn); + prevBtn->clicked().connect(std::bind([=] () + { + _playPrevious.emit(); + })); - Wt::WPushButton *nextBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("next", nextBtn); + Wt::WText *nextBtn = new Wt::WText("", Wt::XHTMLText); + nextBtn->addStyleClass("mediaplayer-btn"); + bindWidget("next", nextBtn); + nextBtn->clicked().connect(std::bind([=] () + { + _playNext.emit(); + })); - Wt::WPushButton *playPauseBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("play-pause", playPauseBtn); + Wt::WText *playPauseBtn = new Wt::WText("", Wt::XHTMLText); + playPauseBtn->addStyleClass("mediaplayer-btn"); + bindWidget("play-pause", playPauseBtn); Wt::WText *trackCurrentTime = new Wt::WText("00:00"); - t->bindWidget("curtime", trackCurrentTime); + bindWidget("curtime", trackCurrentTime); _trackDuration = new Wt::WText("00:00"); - t->bindWidget("duration", _trackDuration); + 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 = " + trackCurrentTime->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 updateUI() {\ - document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\ - document.lms.audio.seekbar.value = document.lms.audio.curTime;\ - }\ + "\ + 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 = " + trackCurrentTime->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 updateUI() {\ + document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\ + document.lms.audio.seekbar.value = document.lms.audio.curTime;\ + }\ \ var mouseDown = 0;\ function seekMouseDown(e) {\ ++mouseDown;\ }\ - function seekMouseUp(e) {\ - --mouseDown;\ - }\ + function seekMouseUp(e) {\ + --mouseDown;\ + }\ \ function seeking(e) {\ if (document.lms.audio.state == \"init\")\ return;\ - \ - document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\ + \ + document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\ }\ \ 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 = ~~document.lms.audio.curTime + \" \";\ + \ + 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 = ~~document.lms.audio.curTime + \" \";\ }\ \ function volumeChanged() {\ @@ -208,12 +258,12 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) 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();\ - \ + \ + if (document.lms.audio.audio.paused)\ + document.lms.audio.audio.play();\ + else\ + document.lms.audio.audio.pause();\ + \ }\ \ document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \ @@ -223,8 +273,8 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\ document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\ document.lms.audio.playPause.addEventListener('click', playPause);\ - " - ); + " + ); } diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp index f202e93e..fdc81607 100644 --- a/src/ui/audio/AudioPlayer.hpp +++ b/src/ui/audio/AudioPlayer.hpp @@ -19,23 +19,38 @@ #pragma once +#include +#include + #include "common/InputRange.hpp" #include "database/Types.hpp" namespace UserInterface { -class AudioPlayer : public Wt::WContainerWidget +class AudioPlayer : public Wt::WTemplate { public: AudioPlayer(Wt::WContainerWidget *parent = 0); - void loadTrack(Database::Track::id_type trackId); + bool loadTrack(Database::Track::id_type trackId); - // Slots + // Signals + Wt::Signal& playbackEnded() {return _playbackEnded;} + Wt::Signal& playNext() {return _playNext;} + Wt::Signal& playPrevious() {return _playPrevious;} + Wt::Signal& shuffle() {return _shuffle;} + Wt::Signal& loop() {return _loop;} private: + // Signals + Wt::Signal _playbackEnded; + Wt::Signal _playNext; + Wt::Signal _playPrevious; + Wt::Signal _shuffle; + Wt::Signal _loop; + Wt::WAudio* _audio; Wt::WText* _trackDuration; Wt::WText* _trackName; diff --git a/src/ui/audio/desktop/AudioMediaPlayer.cpp b/src/ui/audio/desktop/AudioMediaPlayer.cpp deleted file mode 100644 index 42d9e6cb..00000000 --- a/src/ui/audio/desktop/AudioMediaPlayer.cpp +++ /dev/null @@ -1,289 +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 . - */ - -#include -#include -#include -#include -#include - -#include "logger/Logger.hpp" - -#include "LmsApplication.hpp" - -#include "AudioMediaPlayer.hpp" - -namespace UserInterface { -namespace Desktop { - -Wt::WMediaPlayer::Encoding -AudioMediaPlayer::getBestEncoding() -{ - // MP3 seems to be better supported everywhere - return Wt::WMediaPlayer::MP3; -} - -AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent) - : Wt::WContainerWidget(parent), - _mediaResource(nullptr) -{ - this->setStyleClass("mediaplayer"); - - Wt::WVBoxLayout* mainLayout = new Wt::WVBoxLayout(); - this->setLayout(mainLayout); - - // Determine the encoding to be used - { - Wt::Dbo::Transaction transaction(DboSession()); - switch (CurrentUser()->getAudioEncoding()) - { - case Database::AudioEncoding::MP3: _encoding = Wt::WMediaPlayer::MP3; break; - case Database::AudioEncoding::WEBMA: _encoding = Wt::WMediaPlayer::WEBMA; break; - case Database::AudioEncoding::OGA: _encoding = Wt::WMediaPlayer::OGA; break; - case Database::AudioEncoding::AUTO: - default: - _encoding = getBestEncoding(); - } - } - - LMS_LOG(UI, INFO) << "Audio player using encoding " << _encoding; - - // Current Media info - Wt::WHBoxLayout *currentMediaLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(currentMediaLayout, 1); - - currentMediaLayout->addWidget( _mediaCover = new Wt::WImage()); - _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(72)); - _mediaCover->setStyleClass("mediaplayer-current-cover"); - - Wt::WVBoxLayout* mediaInfoLayout = new Wt::WVBoxLayout(); - currentMediaLayout->addLayout(mediaInfoLayout, 1); - - mediaInfoLayout->addWidget( _mediaTitle = new Wt::WText("---")); - mediaInfoLayout->addWidget( _mediaArtistRelease = new Wt::WText("---")); - _mediaTitle->setStyleClass("mediaplayer-current-track vertical-align"); - _mediaArtistRelease->setStyleClass("mediaplayer-current-artist vertical-align"); - - // Time control - Wt::WHBoxLayout *sliderLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(sliderLayout); - - sliderLayout->addWidget(_curTime = new Wt::WText("00:00:00")); - sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1); - sliderLayout->addWidget(_duration = new Wt::WText("00:00:00")); - _timeSlider->setHeight(26); // Default is too big (50) - _curTime->setStyleClass("vertical-align"); - _duration->setStyleClass("vertical-align"); - - // Controls - Wt::WHBoxLayout *controlsLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(controlsLayout); - - - Wt::WPushButton *prevBtn = new Wt::WPushButton("<<"); - controlsLayout->addWidget(prevBtn); - prevBtn->setStyleClass("mediaplayer-btn-controls"); - - Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget();; - - _playBtn = new Wt::WPushButton("Play"); - btnContainer->addWidget(_playBtn); - _playBtn->setWidth(70); - _playBtn->setStyleClass("mediaplayer-btn-controls"); - - _pauseBtn = new Wt::WPushButton("Pause"); - btnContainer->addWidget(_pauseBtn); - _pauseBtn->setWidth(70); - _pauseBtn->setStyleClass("mediaplayer-btn-controls"); - - controlsLayout->addWidget(btnContainer); - - Wt::WPushButton *nextBtn = new Wt::WPushButton(">>"); - controlsLayout->addWidget(nextBtn); - nextBtn->setStyleClass("mediaplayer-btn-controls"); - - _volumeSlider = new Wt::WSlider(); - _volumeSlider->setRange(0,100); - _volumeSlider->setWidth(60); // Default is too big (150) - _volumeSlider->setHeight(26); // Default is too big (50) - _volumeSlider->setMinimumSize(50, Wt::WLength::Auto); - controlsLayout->addWidget(_volumeSlider, 1); - - Wt::WPushButton *loop = new Wt::WPushButton("Loop"); - loop->setCheckable(true); - loop->setStyleClass("btn-xs"); - loop->checked().connect(std::bind([=] () { _loop.emit( true ); })); - loop->unChecked().connect(std::bind([=] () { _loop.emit( false ); })); - controlsLayout->addWidget(loop); - - Wt::WPushButton *shuffle = new Wt::WPushButton("Shuffle"); - shuffle->setCheckable(true); - shuffle->setStyleClass("btn-xs"); - shuffle->checked().connect(std::bind([=] () { _shuffle.emit( true ); })); - shuffle->unChecked().connect(std::bind([=] () { _shuffle.emit( false );})); - controlsLayout->addWidget(shuffle); - - _mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, btnContainer ); - _mediaPlayer->addSource( _encoding, "" ); - _mediaPlayer->ended().connect(this, &AudioMediaPlayer::handleTrackEnded); - - _mediaPlayer->setControlsWidget( 0 ); - _mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn); - _mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn); - - _mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated); - - _volumeSlider->setValue(_mediaPlayer->volume() * 100); - - nextBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playNext.emit(); - })); - - prevBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playPrevious.emit(); - })); - _timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset); - _timeSlider->setDisabled(true); - - _volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved); - -} - -void -AudioMediaPlayer::loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters) -{ - _currentFile = filePath; - _currentParameters = parameters; - - _mediaPlayer->clearSources(); - - if (_mediaResource) - delete _mediaResource; - - _mediaResource = new AvConvTranscodeStreamResource( filePath, parameters, this ); - - _mediaPlayer->addSource( getEncoding(), Wt::WLink(_mediaResource)); - - // Auto play - _mediaPlayer->play(); -} - -bool -AudioMediaPlayer::load(Database::Track::id_type trackId) -{ - Av::TranscodeParameters parameters; - - boost::filesystem::path path; - - boost::posix_time::time_duration duration; - - { - Wt::Dbo::Transaction transaction(DboSession()); - - Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); - if (!track) - { - LMS_LOG(UI, INFO) << "Cannot find track id " << trackId; - return false; - } - - path = track->getPath(); - parameters.setBitrate(Av::Stream::Type::Audio, CurrentUser()->getAudioBitrate() ); - - duration = track->getDuration(); - - _mediaTitle->setText ( Wt::WString::fromUTF8(track->getName()) ); - _mediaArtistRelease->setText ( Wt::WString::fromUTF8(track->getArtist()->getName()) + " - " + Wt::WString::fromUTF8(track->getRelease()->getName()) ); - _mediaCover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 72))); - } - - Av::MediaFile mediaFile(path); - - if (!mediaFile.open() || !mediaFile.scan()) - { - LMS_LOG(UI, INFO) << "Cannot open file '" << path << "'"; - return false; - } - - // It seems to be far better to manually map the streams - // otherwise, some files may have to be fully transcoded to be played by browser... - int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio); - if (audioBestStreamId != -1) - parameters.addStream(audioBestStreamId); - - Av::Encoding encoding; - switch (_encoding) - { - case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break; - case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break; - case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break; - default: - encoding = Av::Encoding::MP3; - } - parameters.setEncoding(encoding); - - _timeSlider->setDisabled(false); - _timeSlider->setRange(0, duration.total_seconds() ); - _timeSlider->setValue(0); - - _duration->setText( boost::posix_time::to_simple_string( duration )); - - loadPlayer(path, parameters); - - return true; -} - -void -AudioMediaPlayer::handlePlayOffset(int offsetSecs) -{ - Av::TranscodeParameters parameters = _currentParameters; - - parameters.setOffset( boost::posix_time::seconds(offsetSecs) ); - - loadPlayer(_currentFile, parameters); -} - -void -AudioMediaPlayer::handleTrackEnded(void) -{ - LMS_LOG(UI, INFO) << "Track ended!"; - _playbackEnded.emit(); -} - -void -AudioMediaPlayer::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 -AudioMediaPlayer::handleVolumeSliderMoved(int value) -{ - _mediaPlayer->setVolume( value / 100. ); -} - -} // namespace Desktop -} // namespace UserInterface diff --git a/src/ui/audio/desktop/AudioMediaPlayer.hpp b/src/ui/audio/desktop/AudioMediaPlayer.hpp deleted file mode 100644 index 4c3b4416..00000000 --- a/src/ui/audio/desktop/AudioMediaPlayer.hpp +++ /dev/null @@ -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 . - */ - -#ifndef __AUDIO_MEDIA_PLAYER_HPP -#define __AUDIO_MEDIA_PLAYER_HPP - -#include - -#include -#include -#include -#include -#include -#include - -#include "database/Types.hpp" - -#include "av/AvTranscoder.hpp" -#include "resource/AvConvTranscodeStreamResource.hpp" -#include "resource/CoverResource.hpp" - -namespace UserInterface { -namespace Desktop { - -class AudioMediaPlayer : public Wt::WContainerWidget -{ - public: - static Wt::WMediaPlayer::Encoding getBestEncoding(); - - AudioMediaPlayer(Wt::WContainerWidget *parent = 0); - - // Load Media to be played - bool load(Database::Track::id_type trackId); - - // Accessors - Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; } - - // Signal slots - Wt::Signal& playbackEnded() {return _playbackEnded;} - Wt::Signal& playNext() {return _playNext;} - Wt::Signal& playPrevious() {return _playPrevious;} - Wt::Signal& shuffle() {return _shuffle;} - Wt::Signal& loop() {return _loop;} - - - private: - - void handlePlayOffset(int offsetSecs); - void handleTrackEnded(void); - - void handleValueChanged(double); - void handleTimeUpdated(void); - void handleSliderMoved(int value); - - void handleVolumeSliderMoved(int value); - - void loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters); - - // Signals - Wt::Signal _playbackEnded; - Wt::Signal _playNext; - Wt::Signal _playPrevious; - Wt::Signal _shuffle; - Wt::Signal _loop; - - // Core - Wt::WMediaPlayer* _mediaPlayer; - AvConvTranscodeStreamResource* _mediaResource; - Wt::WMediaPlayer::Encoding _encoding; - - // Media info - Wt::WImage* _mediaCover; - Wt::WText* _mediaTitle; - Wt::WText* _mediaArtistRelease; - - // Controls - Wt::WPushButton* _playBtn; - Wt::WPushButton* _pauseBtn; - Wt::WSlider* _timeSlider; - Wt::WSlider* _volumeSlider; - Wt::WText* _curTime; - Wt::WText* _duration; - - // Transcode - boost::filesystem::path _currentFile; - Av::TranscodeParameters _currentParameters; - -}; - -} // namespace Desktop -} // namespace UserInterface - -#endif - diff --git a/src/ui/audio/desktop/DesktopAudio.cpp b/src/ui/audio/desktop/DesktopAudio.cpp index 7e898a78..efcd13c3 100644 --- a/src/ui/audio/desktop/DesktopAudio.cpp +++ b/src/ui/audio/desktop/DesktopAudio.cpp @@ -120,73 +120,72 @@ _playQueue(nullptr) _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); + } + + Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); + upBtn->setStyleClass("btn-sm"); + playlistControls->addWidget(upBtn); + Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); + downBtn->setStyleClass("btn-sm"); + playlistControls->addWidget(downBtn); + Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); + delBtn->setStyleClass("btn-sm btn-warning"); + playlistControls->addWidget(delBtn); + Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR"); + 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(_playQueue, &PlayQueue::delAll); + + 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()); - - Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget(); - playQueueContainer->setStyleClass("playqueue"); - Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); - playQueueContainer->setLayout(playQueueLayout); - - _mediaPlayer = new AudioMediaPlayer(); - playQueueLayout->addWidget(_mediaPlayer); - - 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); - } - - Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); - upBtn->setStyleClass("btn-sm"); - playlistControls->addWidget(upBtn); - Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); - downBtn->setStyleClass("btn-sm"); - playlistControls->addWidget(downBtn); - Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); - delBtn->setStyleClass("btn-sm btn-warning"); - playlistControls->addWidget(delBtn); - Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR"); - 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(_playQueue, &PlayQueue::delAll); - - - playQueueLayout->addLayout(playlistControls); - - mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1); - - // Load the last known queue - playlistLoadToPlayqueue(CurrentQueuePlaylistName); - // Select the last known playing track _playQueue->select(CurrentUser()->getCurPlayingTrackPos()); } + _mediaPlayer = new AudioPlayer(); + 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); + mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel)); // Double click on track // Set the selected tracks to the play queue @@ -510,7 +509,7 @@ Audio::playTrack(Track::id_type trackId, int pos) CurrentUser().modify()->setCurPlayingTrackPos(pos); } - if (!_mediaPlayer->load(trackId)) + if (!_mediaPlayer->loadTrack(trackId)) _playQueue->playNext(); } diff --git a/src/ui/audio/desktop/DesktopAudio.hpp b/src/ui/audio/desktop/DesktopAudio.hpp index f5ee34ec..ac7bda65 100644 --- a/src/ui/audio/desktop/DesktopAudio.hpp +++ b/src/ui/audio/desktop/DesktopAudio.hpp @@ -17,14 +17,13 @@ * along with LMS. If not, see . */ -#ifndef UI_AUDIO_DESKTOP_HPP -#define UI_AUDIO_DESKTOP_HPP +#pragma once #include #include -#include "AudioMediaPlayer.hpp" +#include "audio/AudioPlayer.hpp" #include "TrackView.hpp" #include "PlayQueue.hpp" @@ -66,7 +65,7 @@ class Audio : public UserInterface::Audio void handlePlaylistSelected(Wt::WString name); - AudioMediaPlayer* _mediaPlayer; + AudioPlayer* _mediaPlayer; TrackView* _trackView; PlayQueue* _playQueue; @@ -80,5 +79,3 @@ class Audio : public UserInterface::Audio } // namespace Desktop } // namespace UserInterface -#endif - From 18a979a67ae2f88ef424a45a39b7b84c2ea24b4f Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 13 Jan 2016 19:03:53 +0100 Subject: [PATCH 19/29] [UI] Correct a crash due to concurrent accesses to a dbo session --- src/ui/resource/CoverResource.cpp | 8 ++-- src/ui/resource/CoverResource.hpp | 1 + src/ui/resource/TranscodeResource.cpp | 65 ++++++++++++++------------- src/ui/resource/TranscodeResource.hpp | 1 - 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index 885ad94b..54e03972 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -21,6 +21,7 @@ #include #include "logger/Logger.hpp" +#include "LmsApplication.hpp" #include "cover/CoverArtGrabber.hpp" @@ -36,7 +37,7 @@ _db(db) { } -CoverResource:: ~CoverResource() +CoverResource::~CoverResource() { beingDeleted(); } @@ -121,7 +122,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons { // transactions are not thread safe - std::unique_lock lock(_mutex); + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); Wt::Dbo::Transaction transaction(_db.getSession()); @@ -147,8 +148,9 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons else if (releaseIdStr) { Database::Release::id_type releaseId = std::stol(*releaseIdStr); + // transactions are not thread safe - std::unique_lock lock(_mutex); + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); Wt::Dbo::Transaction transaction(_db.getSession()); covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), releaseId); diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index abd14cd0..9a8ee139 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -51,6 +51,7 @@ class CoverResource : public Wt::WResource Image::Image getDefaultCover(std::size_t size); void putCover(Wt::Http::Response& response, Image::Image image); + // Used to protect transactions since they are not thread safe std::mutex _mutex; Database::Handler& _db; diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index 16478a5f..b0a31482 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -20,6 +20,7 @@ #include #include "logger/Logger.hpp" +#include "LmsApplication.hpp" #include "TranscodeResource.hpp" @@ -94,41 +95,41 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, Database::Track::id_type trackId = std::stol(*trackIdStr); // transactions are not thread safe - std::unique_lock lock(_mutex); - - Wt::Dbo::Transaction transaction(_db.getSession()); - - Database::User::pointer user = _db.getCurrentUser(); - Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); - - if (!track) { - LMS_LOG(UI, ERROR) << "Missing track"; - return; + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); + + Wt::Dbo::Transaction transaction(_db.getSession()); + + Database::User::pointer user = _db.getCurrentUser(); + Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); + + if (!track) + { + LMS_LOG(UI, ERROR) << "Missing track"; + return; + } + + if (!user) + { + LMS_LOG(UI, ERROR) << "Missing user"; + return; + } + + Av::TranscodeParameters parameters; + + parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr))); + parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr))); + parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() ); + for (std::string strStream: streams) + { + LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream); + parameters.addStream(std::stol(strStream)); + } + + LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset(); + transcoder = std::make_shared(track->getPath(), parameters); } - if (!user) - { - LMS_LOG(UI, ERROR) << "Missing user"; - return; - } - - Av::TranscodeParameters parameters; - - parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr))); - parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr))); - parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() ); - for (std::string strStream: streams) - { - LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream); - parameters.addStream(std::stol(strStream)); - } - - LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset(); - transcoder = std::make_shared(track->getPath(), parameters); - - transaction.commit(); - LMS_LOG(UI, DEBUG) << "Mime type set to '" << Av::encoding_to_mimetype(Av::Encoding::MP3); response.setMimeType( Av::encoding_to_mimetype(Av::Encoding::MP3) ); diff --git a/src/ui/resource/TranscodeResource.hpp b/src/ui/resource/TranscodeResource.hpp index e4714250..2fc2ad1d 100644 --- a/src/ui/resource/TranscodeResource.hpp +++ b/src/ui/resource/TranscodeResource.hpp @@ -42,7 +42,6 @@ class TranscodeResource : public Wt::WResource private: - std::mutex _mutex; Database::Handler& _db; static const std::size_t _bufferSize = 65536; From b68fb38e6e9afb8c5d1f539e4bff988e6afbb2f0 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 13 Jan 2016 19:05:37 +0100 Subject: [PATCH 20/29] [UI] play/pause icon switching + better looking --- TODO | 2 +- approot/templates.xml | 6 +++++- docroot/css/lms.css | 31 ++++++++++++++++++++--------- src/ui/audio/AudioPlayer.cpp | 32 +++++++++++++++++++++--------- src/ui/audio/desktop/PlayQueue.cpp | 4 +--- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/TODO b/TODO index fbffd99b..38834cf1 100644 --- a/TODO +++ b/TODO @@ -51,9 +51,9 @@ - Add a download button to get the current playlist in a streamed zip file - Add a upload button to upload media files in a dedicated directory - Perform a search for each key pressed (using a timer, in JS) + - Save user prefs in local storage (volume, loop, shuffle, current playqueue, etc.) [desktop] - Better style... - - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) - TrackView: Reselect the current selected item when displaying the updated search results - Add keyboard shortcuts [mobile] diff --git a/approot/templates.xml b/approot/templates.xml index 1f8f11a6..8f879ad4 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -529,7 +529,11 @@ - ${audio}${prev}${play-pause}${next}${cover}${artist}${track}${release}${curtime}${seekbar}${duration}${volume}${shuffle}${repeat}${playlist} + ${audio}${prev}${play-pause}${next} +
+ ${cover} +
+ ${artist}${track}${release}${curtime}${seekbar}${duration}${volume}${shuffle}${repeat}${playlist}
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 304be0ac..75168549 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -1,5 +1,6 @@ .main-nav { margin-bottom: 0px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .Wt-hrh2 { @@ -13,6 +14,7 @@ .playqueue { background-color: #EEE; border-radius: 10px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .playqueue-playing { @@ -35,14 +37,6 @@ height: 64px; } - -.mediaplayer-current-cover { - width: 72px; - height: 72px; - border-radius: 8px; - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); -} - .mediaplayer-track { font-weight: bold; font-size: 120%; @@ -59,14 +53,33 @@ justify-content: space-between; align-items: center; height: 72px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); +} + +.mediaplayer-cover { + margin: 8px; + width: 64px; + min-width: 64px; + height: 64px; + min-height: 64px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.mediaplayer-cover img { + border-radius: 8px; + border-radius: 8px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .mediaplayer-btn { margin: 8px; } -.mediaplayer-btn:before { +.mediaplayer-btn i { cursor: pointer; + text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .mediaplayer-btn-active { diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index be5b68c3..ae70f5e4 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -177,7 +177,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) _playNext.emit(); })); - Wt::WText *playPauseBtn = new Wt::WText("", Wt::XHTMLText); + Wt::WText *playPauseBtn = new Wt::WText("", Wt::XHTMLText); playPauseBtn->addStyleClass("mediaplayer-btn"); bindWidget("play-pause", playPauseBtn); @@ -196,19 +196,29 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\ document.lms.audio.curTimeText = " + trackCurrentTime->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 updateUI() {\ document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\ document.lms.audio.seekbar.value = document.lms.audio.curTime;\ @@ -255,18 +265,22 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) updateUI();\ } \ \ - function playPause() {\ + function playPause() { \ if (document.lms.audio.state == \"init\") \ return;\ \ - if (document.lms.audio.audio.paused)\ - document.lms.audio.audio.play();\ - else\ + 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);\ diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index fb11cedd..294131a1 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -278,10 +278,8 @@ _trackSelector(new TrackSelector()) _model = new Wt::WStandardItemModel(0, 3, this); // 0 Column is hidden (track id) - _model->setHeaderData(COLUMN_ID_TRACK_ID, Wt::WString("#")); - _model->setHeaderData(COLUMN_ID_COVER, Wt::WString("Cover")); - _model->setHeaderData(COLUMN_ID_NAME, Wt::WString("Track")); + this->setHeaderHeight(0); this->setModel(_model); this->setSelectionMode(Wt::ExtendedSelection); this->setSortingEnabled(false); From c383d2a1947facf6a5ff24a6869534a2471a4a29 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 13 Jan 2016 19:03:53 +0100 Subject: [PATCH 21/29] [UI] Correct a crash due to concurrent accesses to a dbo session --- src/ui/resource/CoverResource.cpp | 8 ++-- src/ui/resource/CoverResource.hpp | 1 + src/ui/resource/TranscodeResource.cpp | 65 ++++++++++++++------------- src/ui/resource/TranscodeResource.hpp | 1 - 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/ui/resource/CoverResource.cpp b/src/ui/resource/CoverResource.cpp index 885ad94b..54e03972 100644 --- a/src/ui/resource/CoverResource.cpp +++ b/src/ui/resource/CoverResource.cpp @@ -21,6 +21,7 @@ #include #include "logger/Logger.hpp" +#include "LmsApplication.hpp" #include "cover/CoverArtGrabber.hpp" @@ -36,7 +37,7 @@ _db(db) { } -CoverResource:: ~CoverResource() +CoverResource::~CoverResource() { beingDeleted(); } @@ -121,7 +122,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons { // transactions are not thread safe - std::unique_lock lock(_mutex); + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); Wt::Dbo::Transaction transaction(_db.getSession()); @@ -147,8 +148,9 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons else if (releaseIdStr) { Database::Release::id_type releaseId = std::stol(*releaseIdStr); + // transactions are not thread safe - std::unique_lock lock(_mutex); + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); Wt::Dbo::Transaction transaction(_db.getSession()); covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), releaseId); diff --git a/src/ui/resource/CoverResource.hpp b/src/ui/resource/CoverResource.hpp index abd14cd0..9a8ee139 100644 --- a/src/ui/resource/CoverResource.hpp +++ b/src/ui/resource/CoverResource.hpp @@ -51,6 +51,7 @@ class CoverResource : public Wt::WResource Image::Image getDefaultCover(std::size_t size); void putCover(Wt::Http::Response& response, Image::Image image); + // Used to protect transactions since they are not thread safe std::mutex _mutex; Database::Handler& _db; diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index 16478a5f..b0a31482 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -20,6 +20,7 @@ #include #include "logger/Logger.hpp" +#include "LmsApplication.hpp" #include "TranscodeResource.hpp" @@ -94,41 +95,41 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, Database::Track::id_type trackId = std::stol(*trackIdStr); // transactions are not thread safe - std::unique_lock lock(_mutex); - - Wt::Dbo::Transaction transaction(_db.getSession()); - - Database::User::pointer user = _db.getCurrentUser(); - Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); - - if (!track) { - LMS_LOG(UI, ERROR) << "Missing track"; - return; + Wt::WApplication::UpdateLock lock(LmsApplication::instance()); + + Wt::Dbo::Transaction transaction(_db.getSession()); + + Database::User::pointer user = _db.getCurrentUser(); + Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId); + + if (!track) + { + LMS_LOG(UI, ERROR) << "Missing track"; + return; + } + + if (!user) + { + LMS_LOG(UI, ERROR) << "Missing user"; + return; + } + + Av::TranscodeParameters parameters; + + parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr))); + parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr))); + parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() ); + for (std::string strStream: streams) + { + LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream); + parameters.addStream(std::stol(strStream)); + } + + LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset(); + transcoder = std::make_shared(track->getPath(), parameters); } - if (!user) - { - LMS_LOG(UI, ERROR) << "Missing user"; - return; - } - - Av::TranscodeParameters parameters; - - parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr))); - parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr))); - parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() ); - for (std::string strStream: streams) - { - LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream); - parameters.addStream(std::stol(strStream)); - } - - LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset(); - transcoder = std::make_shared(track->getPath(), parameters); - - transaction.commit(); - LMS_LOG(UI, DEBUG) << "Mime type set to '" << Av::encoding_to_mimetype(Av::Encoding::MP3); response.setMimeType( Av::encoding_to_mimetype(Av::Encoding::MP3) ); diff --git a/src/ui/resource/TranscodeResource.hpp b/src/ui/resource/TranscodeResource.hpp index e4714250..2fc2ad1d 100644 --- a/src/ui/resource/TranscodeResource.hpp +++ b/src/ui/resource/TranscodeResource.hpp @@ -42,7 +42,6 @@ class TranscodeResource : public Wt::WResource private: - std::mutex _mutex; Database::Handler& _db; static const std::size_t _bufferSize = 65536; From 514e46a724fd99addb8b0d9da2b1f40233d57ebe Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 16 Jan 2016 16:36:35 +0100 Subject: [PATCH 22/29] [UI] Better looking --- approot/templates.xml | 15 ++++++++++++--- docroot/css/lms.css | 20 ++++++++++++++++++++ src/ui/audio/AudioPlayer.cpp | 21 +++++++++------------ src/ui/audio/AudioPlayer.hpp | 2 -- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/approot/templates.xml b/approot/templates.xml index 8f879ad4..4b7a4e69 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -529,11 +529,20 @@ - ${audio}${prev}${play-pause}${next} -
+ ${audio} + ${prev}${play-pause}${next} + - ${artist}${track}${release}${curtime}${seekbar}${duration}${volume}${shuffle}${repeat}${playlist} +
+ ${track} ${artist}
+ ${seekbar} +
+ ${curtime}${duration} +
+ ${volume} +
+ ${shuffle}${repeat}${playlist} diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 75168549..c6ba8d11 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -86,6 +86,26 @@ color: #428bca; } +.mediaplayer-info-container { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + margin: 8px; +} + +.mediaplayer-volume-container { + width: 24px; + height: 64px; + margin: 8px; +} + +.mediaplayer-volume { + height: 64px; + -webkit-appearance: slider-vertical; + writing-mode: bt-lr; +} + .mobile-search-title { font-weight: bold; height: 32px; diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index ae70f5e4..4fc43104 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -45,9 +45,8 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) return false; } - _trackName->setText(Wt::WString::fromUTF8(track->getName())); - _artistName->setText( Wt::WString::fromUTF8(track->getArtist()->getName())); - _releaseName->setText( Wt::WString::fromUTF8(track->getRelease()->getName())); + bindString("track", Wt::WString::fromUTF8(track->getName())); + bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName())); _cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64)); _trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() )); @@ -111,16 +110,12 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) InputRange *seekbar = new InputRange(); bindWidget("seekbar", seekbar); - _trackName = new Wt::WText(); - bindWidget("track", _trackName); - - _artistName = new Wt::WText(); - bindWidget("artist", _artistName); - - _releaseName = new Wt::WText(); - bindWidget("release", _releaseName); + bindString("track", "Track"); + bindString("artist", "Artist"); InputRange *volumeSlider = new InputRange(); + volumeSlider->addStyleClass("mediaplayer-volume"); + volumeSlider->setAttributeValue("orient", "vertical"); // firefox bindWidget("volume", volumeSlider); Wt::WText *playlistBtn = new Wt::WText("", Wt::XHTMLText); @@ -162,7 +157,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) })); Wt::WText *prevBtn = new Wt::WText("", Wt::XHTMLText); - prevBtn->addStyleClass("mediaplayer-btn"); + prevBtn->addStyleClass("mediaplayer-btn hidden-xs"); bindWidget("prev", prevBtn); prevBtn->clicked().connect(std::bind([=] () { @@ -182,9 +177,11 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) bindWidget("play-pause", playPauseBtn); Wt::WText *trackCurrentTime = new Wt::WText("00:00"); + trackCurrentTime->addStyleClass("hidden-xs"); bindWidget("curtime", trackCurrentTime); _trackDuration = new Wt::WText("00:00"); + _trackDuration->addStyleClass("hidden-xs"); bindWidget("duration", _trackDuration); this->doJavaScript( diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp index fdc81607..c2fd80c9 100644 --- a/src/ui/audio/AudioPlayer.hpp +++ b/src/ui/audio/AudioPlayer.hpp @@ -54,8 +54,6 @@ class AudioPlayer : public Wt::WTemplate Wt::WAudio* _audio; Wt::WText* _trackDuration; Wt::WText* _trackName; - Wt::WText* _artistName; - Wt::WText* _releaseName; Wt::WImage* _cover; }; From 027e9d9bf70f78142fef3c12303705d3cc261218 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 16 Jan 2016 17:36:47 +0100 Subject: [PATCH 23/29] [UI] Better looking --- approot/templates.xml | 15 +++++++++------ docroot/css/lms.css | 27 +++++++++++++++++++++++++-- src/ui/audio/AudioPlayer.cpp | 5 +++-- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/approot/templates.xml b/approot/templates.xml index 4b7a4e69..c6e1c600 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -531,16 +531,19 @@ ${audio} ${prev}${play-pause}${next} +
+ ${volume} +
- ${track} ${artist}
- ${seekbar} -
- ${curtime}${duration} -
- ${volume} +
+ ${track} ${artist} +
+
+ ${curtime}${seekbar}${duration} +
${shuffle}${repeat}${playlist}
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index c6ba8d11..6c0bf27d 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -87,11 +87,34 @@ } .mediaplayer-info-container { + width: 100%; + min-width: 100px; + margin: 8px; +} + +.mediaplayer-track-info { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - width: 100%; - margin: 8px; +} + +.mediaplayer-progress-container { + display: flex; + align-items: center; +} + +.mediaplayer-badge { + min-width: initial; +} + +.mediaplayer-current-duration { + margin-right: 8px; +} +.mediaplayer-total-duration { + margin-left: 8px; +} + +.mediaplayer-seekbar { } .mediaplayer-volume-container { diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index 4fc43104..b8498493 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -108,6 +108,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) _cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64)); InputRange *seekbar = new InputRange(); + seekbar->addStyleClass("mediaplayer-seekbar"); bindWidget("seekbar", seekbar); bindString("track", "Track"); @@ -177,11 +178,11 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) bindWidget("play-pause", playPauseBtn); Wt::WText *trackCurrentTime = new Wt::WText("00:00"); - trackCurrentTime->addStyleClass("hidden-xs"); + trackCurrentTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration"); bindWidget("curtime", trackCurrentTime); _trackDuration = new Wt::WText("00:00"); - _trackDuration->addStyleClass("hidden-xs"); + _trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration"); bindWidget("duration", _trackDuration); this->doJavaScript( From 5151d779ae6067abbf2e6eafde5cb8946fd42bf8 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 16 Jan 2016 19:04:46 +0100 Subject: [PATCH 24/29] [UI] Restored audio encoding setting --- src/av/AvTranscoder.hpp | 2 ++ src/ui/audio/AudioPlayer.cpp | 33 ++++++++++++++++++++++- src/ui/audio/AudioPlayer.hpp | 2 ++ src/ui/resource/TranscodeResource.cpp | 12 +++++---- src/ui/settings/SettingsAudioFormView.cpp | 2 +- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp index 5706ea94..7e671fb8 100644 --- a/src/av/AvTranscoder.hpp +++ b/src/av/AvTranscoder.hpp @@ -91,6 +91,8 @@ class Transcoder void process(std::vector& output, std::size_t maxSize); bool isComplete(void) { return _isComplete; } + const TranscodeParameters& getParameters() const { return _parameters; } + private: Transcoder(); diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index b8498493..7df769c1 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -33,9 +33,19 @@ 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); @@ -45,6 +55,25 @@ AudioPlayer::loadTrack(Database::Track::id_type 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(SessionCoverResource()->getTrackUrl(trackId, 64)); @@ -75,11 +104,13 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) 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(); //TODO, encoding - _audio->addSource(SessionTranscodeResource()->getUrl(trackId, Av::Encoding::MP3, 0, streams)); + _audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams)); _audio->setPreloadMode(Wt::WAudio::PreloadAuto); _audio->play(); diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp index c2fd80c9..0527f2fa 100644 --- a/src/ui/audio/AudioPlayer.hpp +++ b/src/ui/audio/AudioPlayer.hpp @@ -24,6 +24,7 @@ #include "common/InputRange.hpp" #include "database/Types.hpp" +#include "av/AvTranscoder.hpp" namespace UserInterface { @@ -33,6 +34,7 @@ class AudioPlayer : public Wt::WTemplate AudioPlayer(Wt::WContainerWidget *parent = 0); + Av::Encoding getBestEncoding() const; bool loadTrack(Database::Track::id_type trackId); // Signals diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index b0a31482..882e1171 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -130,14 +130,17 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, transcoder = std::make_shared(track->getPath(), parameters); } - LMS_LOG(UI, DEBUG) << "Mime type set to '" << Av::encoding_to_mimetype(Av::Encoding::MP3); - response.setMimeType( Av::encoding_to_mimetype(Av::Encoding::MP3) ); + std::string mimeType = Av::encoding_to_mimetype(transcoder->getParameters().getEncoding()); + + LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'"; + response.setMimeType(mimeType); if (!transcoder->start()) { LMS_LOG(UI, ERROR) << "Cannot start transcoder"; return; } + LMS_LOG(UI, DEBUG) << "Transcoder started"; } @@ -146,21 +149,20 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, std::vector data; data.reserve(_bufferSize); - LMS_LOG(UI, DEBUG) << "Reading data from transcoder"; transcoder->process(data, _bufferSize); response.out().write(reinterpret_cast(&data[0]), data.size()); - LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete(); if (!response.out()) + { LMS_LOG(UI, ERROR) << "Write failed!"; + } } if (!transcoder->isComplete() && response.out()) { continuation = response.createContinuation(); continuation->setData(transcoder); - LMS_LOG(UI, DEBUG) << "Continuation set to " << continuation; } else LMS_LOG(UI, DEBUG) << "No more data!"; diff --git a/src/ui/settings/SettingsAudioFormView.cpp b/src/ui/settings/SettingsAudioFormView.cpp index 1af1fc18..93475c48 100644 --- a/src/ui/settings/SettingsAudioFormView.cpp +++ b/src/ui/settings/SettingsAudioFormView.cpp @@ -64,7 +64,7 @@ class AudioFormModel : public Wt::WFormModel initializeModels(); addField(BitrateField); - addField(EncodingField, "Session must be reloaded to apply this setting"); + addField(EncodingField); setValidator(BitrateField, new Wt::WValidator(true)); // mandatory setValidator(EncodingField, new Wt::WValidator(true)); // mandatory From 1860ab7c15ccd86617e9a5d246f0f216679f2099 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 16 Jan 2016 19:09:53 +0100 Subject: [PATCH 25/29] [DOC] updated TODO --- TODO | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index 38834cf1..1a40dc32 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,10 @@ [Book] -- Make the feature +- Make the feature? [Cover] -- Handle preferred cover file names -- Implement a cache and a grabber from some web service (mandatory for artists) +- Handle preferred cover file names ("front.xxx", "cover.xxx", ...) +- Implement a cache and a grabber from some web service (mandatory for artists?) [Database] - Optim, use SQL query to get the "genre" orphans @@ -13,7 +13,6 @@ - add a global play counter for tracks. This will help people to spot most popular files - rework the exception process in av/metadata/updater in case of bad files - Use the WServer::post method to notify the end of the database scan? (with results?) -- Put more config information in the database, access from the UI [Metadata] - OGG metadata -> properly handle metadata nested in the audio stream @@ -33,7 +32,7 @@ - Handle internationalization - Handle internal path [Settings] - - logout users that are being changed (loss of admin admin rights), or make sure they are still admin when they make changes + - logout users that are being changed (loss of admin rights), or make sure they are still admin when they make changes - "signal not exposed" problem if a user logout and login again. Bad resource destruction? - add a scroll area in order for settings to work on mobile devices [user/transcoding] @@ -57,14 +56,13 @@ - TrackView: Reselect the current selected item when displaying the updated search results - Add keyboard shortcuts [mobile] + - Reorganize the views to ease internal paths - Covers not always the same height - 'duration' and 'play' tags are broken if title is too long - - Remove the extra search widget - Cover loading breaks the vertical alignment - Implement a play queue - Implement a decent player - - Make the release clickable in the Track result view - - Reorganize the views to ease internal paths? + - Make the release/artist clickable when displayed to the user [Video] - implement a decent mediaplayer From ff4dfe2dfd058acd47b255631e265cb169b234f9 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 18 Jan 2016 13:33:49 +0100 Subject: [PATCH 26/29] [UI] Nicer display of current time / time duration --- src/ui/audio/AudioPlayer.cpp | 37 +++++++++++++++++++++++++++--------- src/ui/audio/AudioPlayer.hpp | 1 + 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index 7df769c1..c97e775b 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -26,6 +26,7 @@ #include "common/InputRange.hpp" #include "logger/Logger.hpp" +#include "utils/Utils.hpp" #include "LmsApplication.hpp" @@ -77,7 +78,11 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) bindString("track", Wt::WString::fromUTF8(track->getName())); bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName())); _cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64)); - _trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() )); + + 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()); @@ -208,11 +213,11 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) playPauseBtn->addStyleClass("mediaplayer-btn"); bindWidget("play-pause", playPauseBtn); - Wt::WText *trackCurrentTime = new Wt::WText("00:00"); - trackCurrentTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration"); - bindWidget("curtime", trackCurrentTime); + _trackCurTime = new Wt::WText("--:--"); + _trackCurTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration"); + bindWidget("curtime", _trackCurTime); - _trackDuration = new Wt::WText("00:00"); + _trackDuration = new Wt::WText("--:--"); _trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration"); bindWidget("duration", _trackDuration); @@ -223,7 +228,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) document.lms.audio.audio = " + _audio->jsRef() + ";\ document.lms.audio.seekbar = " + seekbar->jsRef() +";\ document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\ - document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\ + document.lms.audio.curTimeText = " + _trackCurTime->jsRef() + ";\ document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\ \ document.lms.audio.offset = 0;\ @@ -247,9 +252,23 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) 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 = document.lms.audio.curTime;\ + 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;\ }\ \ @@ -265,7 +284,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) if (document.lms.audio.state == \"init\")\ return;\ \ - document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\ + document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.seekbar.value, document.lms.audio.seekbar.max > 3600); \ }\ \ function seek(e) {\ @@ -281,7 +300,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) audioSource.src = src + document.lms.audio.seekbar.value;\ document.lms.audio.audio.load(); \ document.lms.audio.audio.play(); \ - document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\ + document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \ }\ \ function volumeChanged() {\ diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp index 0527f2fa..371fe2b3 100644 --- a/src/ui/audio/AudioPlayer.hpp +++ b/src/ui/audio/AudioPlayer.hpp @@ -54,6 +54,7 @@ class AudioPlayer : public Wt::WTemplate Wt::Signal _loop; Wt::WAudio* _audio; + Wt::WText* _trackCurTime; Wt::WText* _trackDuration; Wt::WText* _trackName; Wt::WImage* _cover; From 4575da4c79a1f823d877b99e914623040c743eb1 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 24 Jan 2016 15:13:36 +0100 Subject: [PATCH 27/29] [UI] Removed unused playlist icon --- approot/templates.xml | 2 +- src/ui/audio/AudioPlayer.cpp | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/approot/templates.xml b/approot/templates.xml index c6e1c600..411523a4 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -545,7 +545,7 @@ ${curtime}${seekbar}${duration}
- ${shuffle}${repeat}${playlist} + ${shuffle}${repeat}
diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index c97e775b..1d166635 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -114,9 +114,8 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) //TODO, try to load everything in JS in order to prevent the WriteError bug? _audio->pause(); _audio->clearSources(); - //TODO, encoding _audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams)); - _audio->setPreloadMode(Wt::WAudio::PreloadAuto); + _audio->setPreloadMode(Wt::WAudio::PreloadNone); _audio->play(); return true; @@ -131,7 +130,7 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) // TODO potential leak here _audio = new Wt::WAudio(); _audio->setOptions(Wt::WAudio::Autoplay); - _audio->setPreloadMode(Wt::WAudio::PreloadAuto); + _audio->setPreloadMode(Wt::WAudio::PreloadNone); bindWidget("audio", _audio); _audio->ended().connect(std::bind([=] () @@ -155,10 +154,6 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) volumeSlider->setAttributeValue("orient", "vertical"); // firefox bindWidget("volume", volumeSlider); - Wt::WText *playlistBtn = new Wt::WText("", Wt::XHTMLText); - playlistBtn->addStyleClass("mediaplayer-btn"); - bindWidget("playlist", playlistBtn); - Wt::WText *repeatBtn = new Wt::WText("", Wt::XHTMLText); repeatBtn->addStyleClass("mediaplayer-btn"); bindWidget("repeat", repeatBtn); From 1234aceeefedbf578b9eeffe2fbe5e4b287ed19c Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 24 Jan 2016 15:14:07 +0100 Subject: [PATCH 28/29] [UI] Changed default transcode chunk size --- src/ui/resource/TranscodeResource.cpp | 4 ++-- src/ui/resource/TranscodeResource.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index 882e1171..8acef15c 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -147,9 +147,9 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, if (!transcoder->isComplete()) { std::vector data; - data.reserve(_bufferSize); + data.reserve(_chunkSize); - transcoder->process(data, _bufferSize); + transcoder->process(data, _chunkSize); response.out().write(reinterpret_cast(&data[0]), data.size()); LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete(); diff --git a/src/ui/resource/TranscodeResource.hpp b/src/ui/resource/TranscodeResource.hpp index 2fc2ad1d..4ed7b4c5 100644 --- a/src/ui/resource/TranscodeResource.hpp +++ b/src/ui/resource/TranscodeResource.hpp @@ -44,7 +44,7 @@ class TranscodeResource : public Wt::WResource Database::Handler& _db; - static const std::size_t _bufferSize = 65536; + static const std::size_t _chunkSize = 65536*4; }; } // namespace UserInterface From 7463e243dd253469f443648bc3555c90e50eb528 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 24 Jan 2016 15:14:27 +0100 Subject: [PATCH 29/29] [DOC] Updated doc --- TODO | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO b/TODO index 1a40dc32..513e9f20 100644 --- a/TODO +++ b/TODO @@ -31,6 +31,7 @@ [UI] - Handle internationalization - Handle internal path + - Cannot log again when refreshing the application in firefox (have to close/open a new browser tab) [Settings] - logout users that are being changed (loss of admin rights), or make sure they are still admin when they make changes - "signal not exposed" problem if a user logout and login again. Bad resource destruction?