From a07704c218c38383d5854b2f1f0b28d2b60ba44f Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 5 Mar 2016 11:34:59 +0100 Subject: [PATCH] [UI/MOBILE] Began internal path support --- approot/templates.xml | 12 ++ src/Makefile.am | 5 +- src/ui/LmsApplication.cpp | 156 ++++++++---------- src/ui/LmsApplication.hpp | 2 + src/ui/audio/mobile/ArtistSearch.cpp | 100 +++++------ src/ui/audio/mobile/ArtistSearch.hpp | 18 +- src/ui/audio/mobile/ArtistSearchView.cpp | 60 +++++++ src/ui/audio/mobile/ArtistSearchView.hpp | 37 +++++ src/ui/audio/mobile/ArtistView.cpp | 55 ++++++ src/ui/audio/mobile/ArtistView.hpp | 37 +++++ src/ui/audio/mobile/MobileAudio.cpp | 132 ++++++--------- src/ui/audio/mobile/MobileAudio.hpp | 10 -- src/ui/audio/mobile/PreviewSearchView.cpp | 79 +++++++++ src/ui/audio/mobile/PreviewSearchView.hpp | 37 +++++ src/ui/audio/mobile/ReleaseSearch.cpp | 21 ++- src/ui/audio/mobile/ReleaseSearch.hpp | 7 +- .../{TrackReleaseView.cpp => ReleaseView.cpp} | 32 +++- .../{TrackReleaseView.hpp => ReleaseView.hpp} | 8 +- src/ui/audio/mobile/SearchUtils.hpp | 32 ++++ src/ui/audio/mobile/TrackSearch.hpp | 3 + test/Makefile.am | 4 +- test/TestWt.cpp | 119 ++++++++++++- 22 files changed, 708 insertions(+), 258 deletions(-) create mode 100644 src/ui/audio/mobile/ArtistSearchView.cpp create mode 100644 src/ui/audio/mobile/ArtistSearchView.hpp create mode 100644 src/ui/audio/mobile/ArtistView.cpp create mode 100644 src/ui/audio/mobile/ArtistView.hpp create mode 100644 src/ui/audio/mobile/PreviewSearchView.cpp create mode 100644 src/ui/audio/mobile/PreviewSearchView.hpp rename src/ui/audio/mobile/{TrackReleaseView.cpp => ReleaseView.cpp} (88%) rename src/ui/audio/mobile/{TrackReleaseView.hpp => ReleaseView.hpp} (89%) create mode 100644 src/ui/audio/mobile/SearchUtils.hpp diff --git a/approot/templates.xml b/approot/templates.xml index 411523a4..01fea403 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -448,6 +448,18 @@ + + + ${title} + ${release-container} + ${show-more} + +
diff --git a/src/Makefile.am b/src/Makefile.am index e6b0f919..67cf739e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -32,9 +32,12 @@ lms_SOURCES = \ $(srcdir)/ui/audio/desktop/TableFilter.cpp \ $(srcdir)/ui/audio/desktop/TrackView.cpp \ $(srcdir)/ui/audio/mobile/ArtistSearch.cpp \ + $(srcdir)/ui/audio/mobile/ArtistSearchView.cpp \ + $(srcdir)/ui/audio/mobile/ArtistView.cpp \ $(srcdir)/ui/audio/mobile/MobileAudio.cpp \ + $(srcdir)/ui/audio/mobile/PreviewSearchView.cpp \ $(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \ - $(srcdir)/ui/audio/mobile/TrackReleaseView.cpp \ + $(srcdir)/ui/audio/mobile/ReleaseView.cpp \ $(srcdir)/ui/audio/mobile/TrackSearch.cpp \ $(srcdir)/ui/common/DirectoryValidator.cpp \ $(srcdir)/ui/common/InputRange.cpp \ diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index be39ef1f..50f31cbb 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -28,8 +28,8 @@ #include #include "config/config.h" - #include "logger/Logger.hpp" +#include "utils/Utils.hpp" #include "settings/Settings.hpp" #include "settings/SettingsFirstConnectionFormView.hpp" @@ -48,20 +48,6 @@ namespace skeletons { extern const char *AuthStrings_xml1; } -namespace { - -bool agentIsMobile() -{ - const Wt::WEnvironment& env = Wt::WApplication::instance()->environment(); - return (env.agentIsIEMobile() - || env.agentIsMobileWebKit() - || env.userAgent().find("Mobile") != std::string::npos // Workaround for firefox - || env.userAgent().find("Tablet") != std::string::npos // Workaround for firefox - ); -} - -} - namespace UserInterface { @@ -177,86 +163,88 @@ LmsApplication::createLmsUI() authWidget->processEnvironment(); root()->addWidget(authWidget); -} + setInternalPath("/"); +} void LmsApplication::handleAuthEvent(void) { - if (DbHandler().getLogin().loggedIn()) - { - LMS_LOG(UI, INFO) << "User '" << CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().agent() << ", session = " << Wt::WApplication::instance()->sessionId(); - - this->root()->setOverflow(Wt::WContainerWidget::OverflowHidden); - setConfirmCloseMessage("Closing LMS. Are you sure?"); - - // Create a Vertical layout: top is the nav bar, bottom is the contents - Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root()); - // Create a navigation bar with a link to a web page. - Wt::WNavigationBar *navigation = new Wt::WNavigationBar(); - navigation->setTitle("LMS", "https://github.com/epoupon/lms"); - navigation->setResponsive(true); - navigation->addStyleClass("main-nav"); - - Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget(); - - contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto); - contentsStack->addStyleClass("contents"); - - Wt::WMenu *menu = new Wt::WMenu(contentsStack); - navigation->addMenu(menu, Wt::AlignRight); - - LineEdit *searchEdit = new LineEdit(500); - navigation->bindWidget("search", searchEdit); - searchEdit->setEmptyText("Search..."); - searchEdit->addStyleClass("navbar-form navbar-nav"); - searchEdit->setWidth(150); - // TODO add a span with a search icon - - Audio *audio; - - if (agentIsMobile()) - audio = new Mobile::Audio(); - else - audio = new Desktop::Audio(); - - menu->addItem("Audio", audio); -#if defined HAVE_VIDEO - menu->addItem("Video", new VideoWidget()); -#endif - 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); - menu->addItem(item); - - popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item) - { - if (item && item->text() == "Logout") - DbHandler().getLogin().logout(); - }, std::placeholders::_1)); - - searchEdit->timedChanged().connect(std::bind([=] () - { - // TODO: check which view is activated and search into it - audio->search(searchEdit->text().toUTF8()); - })); - - - layout->addWidget(navigation); - layout->addWidget(contentsStack, 1); - layout->setContentsMargins(0, 0, 0, 0); - } - else + if (!DbHandler().getLogin().loggedIn()) { LMS_LOG(UI, INFO) << "User logged out, session = " << Wt::WApplication::instance()->sessionId(); quit(""); redirect("/"); + + return; } + + LMS_LOG(UI, INFO) << "User '" << CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().agent() << ", session = " << Wt::WApplication::instance()->sessionId(); + + this->root()->setOverflow(Wt::WContainerWidget::OverflowHidden); + setConfirmCloseMessage("Closing LMS. Are you sure?"); + + // Handle internal paths +// this->setInternalPath("audio"); + + // Create a Vertical layout: top is the nav bar, bottom is the contents + Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root()); + // Create a navigation bar with a link to a web page. + Wt::WNavigationBar *navigation = new Wt::WNavigationBar(); + navigation->setTitle("LMS", "https://github.com/epoupon/lms"); + navigation->setResponsive(true); + navigation->addStyleClass("main-nav"); + + Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget(); + + contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto); + contentsStack->addStyleClass("contents"); + + Wt::WMenu *menu = new Wt::WMenu(contentsStack); + navigation->addMenu(menu, Wt::AlignRight); + + LineEdit *searchEdit = new LineEdit(500); + navigation->bindWidget("search", searchEdit); + searchEdit->setEmptyText("Search..."); + searchEdit->addStyleClass("navbar-form navbar-nav"); + searchEdit->setWidth(150); + // TODO add a span with a search icon + + Mobile::Audio *audio = new Mobile::Audio(); + + menu->addItem("Audio", audio); +#if defined HAVE_VIDEO + menu->addItem("Video", new VideoWidget()); +#endif + 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); + menu->addItem(item); + + popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item) + { + if (item && item->text() == "Logout") + DbHandler().getLogin().logout(); + }, std::placeholders::_1)); + + searchEdit->timedChanged().connect(std::bind([=] () + { + // TODO: check which view is activated and search into it + audio->search(searchEdit->text().toUTF8()); + })); + + layout->addWidget(navigation); + layout->addWidget(contentsStack, 1); + layout->setContentsMargins(0, 0, 0, 0); + + // Set initial path + wApp->setInternalPath("/audio/search/preview", true); } } // namespace UserInterface diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index 447c1db3..18953981 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -56,6 +56,8 @@ class LmsApplication : public Wt::WApplication }; // Helpers to get session data +#define LmsApp LmsApplication::instance() + Database::Handler& DbHandler(); Wt::Dbo::Session& DboSession(); diff --git a/src/ui/audio/mobile/ArtistSearch.cpp b/src/ui/audio/mobile/ArtistSearch.cpp index 35033792..a6b6e1d8 100644 --- a/src/ui/audio/mobile/ArtistSearch.cpp +++ b/src/ui/audio/mobile/ArtistSearch.cpp @@ -31,89 +31,95 @@ namespace Mobile { using namespace Database; -ArtistSearch::ArtistSearch(Wt::WContainerWidget *parent) +ArtistSearch::ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent), -_resCount(0) +_count(0) { - Wt::WTemplate *title = new Wt::WTemplate(this); - title->setTemplateText(Wt::WString::tr("mobile-search-title")); + Wt::WTemplate* artistSearch = new Wt::WTemplate(this); + artistSearch->setTemplateText(Wt::WString::tr("wa-artist-search")); - title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText)); + Wt::WTemplate *titleTemplate = new Wt::WTemplate(this); + titleTemplate->setTemplateText(Wt::WString::tr("mobile-search-title")); + titleTemplate->bindString("text", title); + + artistSearch->bindWidget("title", titleTemplate); - Wt::WTemplate* artistWrapper = new Wt::WTemplate(this); - artistWrapper->setTemplateText(Wt::WString::tr("wa-artist-wrapper")); _contents = new Wt::WContainerWidget(); - artistWrapper->bindWidget("contents", _contents ); + artistSearch->bindWidget("release-container", _contents ); + + _showMore = new Wt::WTemplate(); + _showMore->setTemplateText(Wt::WString::tr("mobile-search-more")); + _showMore->bindString("text", "Tap to show more results..."); + _showMore->hide(); + _showMore->clicked().connect(std::bind([=] { + _sigShowMore.emit(); + })); + + artistSearch->bindWidget("show-more", _showMore); } void ArtistSearch::clear() { - while (count() > 1) - removeWidget(this->widget(1)); - - _resCount = 0; + _contents->clear(); + _count = 0; + _showMore->hide(); } void ArtistSearch::search(Database::SearchFilter filter, size_t nb) { + _filter = filter; + clear(); - addResults(filter, nb); + addResults(nb); } -void -ArtistSearch::addResults(Database::SearchFilter filter, std::size_t nb) +static +std::vector +getArtists(SearchFilter filter, size_t offset, size_t nb, bool &moreResults) { - Wt::Dbo::Transaction transaction(DboSession()); + std::vector artists = Artist::getByFilter(DboSession(), filter, offset, nb + 1); - std::vector artists = Artist::getByFilter(DboSession(), filter, _resCount, nb + 1); - - bool expectMoreResults; if (artists.size() == nb + 1) { - expectMoreResults = true; + moreResults = true; artists.pop_back(); } else - expectMoreResults = false; + moreResults = false; + + return artists; +} + +void +ArtistSearch::addResults(std::size_t nb) +{ + Wt::Dbo::Transaction transaction(DboSession()); + + bool moreResults; + std::vector artists = getArtists(_filter, _count, nb, moreResults); for (Artist::pointer artist : artists) { - Wt::WTemplate* res = new Wt::WTemplate(this); + Wt::WTemplate* res = new Wt::WTemplate(_contents); res->setTemplateText(Wt::WString::tr("wa-artist-res")); - Wt::WImage *artistImg = new Wt::WImage(); + Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt:: WLink::InternalPath, "/audio/artist/" + std::to_string(artist.id()))); + Wt::WImage *artistImg = new Wt::WImage(coverAnchor); artistImg->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); - - res->clicked().connect(std::bind([=] { - _sigArtistSelected(artist.id()); - })); + res->bindWidget("gif", coverAnchor); + res->bindString("name", Wt::WString::fromUTF8(artist->getName(), Wt::PlainText)); } - _resCount += artists.size();; - - if (expectMoreResults) - { - 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([=] { - _sigMoreArtistsSelected(); - removeWidget(moreRes); - - addResults(filter, 20); - })); - } + _count += artists.size(); + if (moreResults) + _showMore->show(); + else + _showMore->hide(); } } // namespace Mobile diff --git a/src/ui/audio/mobile/ArtistSearch.hpp b/src/ui/audio/mobile/ArtistSearch.hpp index f1218c9c..50dc22de 100644 --- a/src/ui/audio/mobile/ArtistSearch.hpp +++ b/src/ui/audio/mobile/ArtistSearch.hpp @@ -23,6 +23,7 @@ #include #include "database/Types.hpp" +#include "database/SearchFilter.hpp" namespace UserInterface { namespace Mobile { @@ -31,24 +32,25 @@ class ArtistSearch : public Wt::WContainerWidget { public: - ArtistSearch(Wt::WContainerWidget *parent = 0); + ArtistSearch(Wt::WString title, Wt::WContainerWidget *parent = 0); void search(Database::SearchFilter filter, std::size_t nb); + void addResults(std::size_t nb); // Slots - Wt::Signal& artistSelected() { return _sigArtistSelected;} - Wt::Signal& moreArtistsSelected() { return _sigMoreArtistsSelected;} + Wt::Signal& showMore() { return _sigShowMore;} private: - Wt::Signal _sigArtistSelected; - Wt::Signal _sigMoreArtistsSelected; + Wt::Signal _sigShowMore; void clear(void); - void addResults(Database::SearchFilter filter, size_t nb); - Wt::WContainerWidget* _contents; - std::size_t _resCount; + Wt::WTemplate* _showMore; + + Database::SearchFilter _filter; + Wt::WContainerWidget* _contents; + std::size_t _count; }; } // namespace Mobile diff --git a/src/ui/audio/mobile/ArtistSearchView.cpp b/src/ui/audio/mobile/ArtistSearchView.cpp new file mode 100644 index 00000000..1fda1e0d --- /dev/null +++ b/src/ui/audio/mobile/ArtistSearchView.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "logger/Logger.hpp" +#include "SearchUtils.hpp" + +#include "ArtistSearch.hpp" + +#include "ArtistSearchView.hpp" + +namespace UserInterface { +namespace Mobile { + +#define SEARCH_NB_ITEMS 20 + +using namespace Database; + +ArtistSearchView::ArtistSearchView(Wt::WContainerWidget* parent) +{ + ArtistSearch* artistSearch = new ArtistSearch("Artists", this); + artistSearch->showMore().connect(std::bind([=] { + artistSearch->addResults(SEARCH_NB_ITEMS); + })); + + wApp->internalPathChanged().connect(std::bind([=] (std::string path) + { + const std::string pathPrefix = "/audio/search/artist"; + + if (!wApp->internalPathMatches(pathPrefix)) + return; + + std::vector keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length())); + + artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS); + + }, std::placeholders::_1)); +} + +} // namespace Mobile +} // namespace UserInterface + diff --git a/src/ui/audio/mobile/ArtistSearchView.hpp b/src/ui/audio/mobile/ArtistSearchView.hpp new file mode 100644 index 00000000..29971950 --- /dev/null +++ b/src/ui/audio/mobile/ArtistSearchView.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface { +namespace Mobile { + +class ArtistSearchView : public Wt::WContainerWidget +{ + public: + + ArtistSearchView(Wt::WContainerWidget* parent = 0); + +}; + +} // namespace Mobile +} // namespace UserInterface + diff --git a/src/ui/audio/mobile/ArtistView.cpp b/src/ui/audio/mobile/ArtistView.cpp new file mode 100644 index 00000000..0a48fb5f --- /dev/null +++ b/src/ui/audio/mobile/ArtistView.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include + +#include "utils/Utils.hpp" + +#include "ReleaseSearch.hpp" +#include "ArtistView.hpp" + +namespace UserInterface { +namespace Mobile { + +using namespace Database; + +ArtistView::ArtistView(Wt::WContainerWidget *parent) +: Wt::WContainerWidget(parent) +{ + ReleaseSearch *releases = new ReleaseSearch(this); + + wApp->internalPathChanged().connect(std::bind([=] (std::string path) + { + const std::string pathPrefix = "/audio/artist/"; + + if (!wApp->internalPathMatches(pathPrefix)) + return; + + std::string strId = path.substr(pathPrefix.length()); + + Artist::id_type id; + if (readAs(strId, id)) + releases->search(SearchFilter::ById(SearchFilter::Field::Artist, id), 20); + + }, std::placeholders::_1)); +} + +} //namespace Mobile +} //namespace UserInterface + diff --git a/src/ui/audio/mobile/ArtistView.hpp b/src/ui/audio/mobile/ArtistView.hpp new file mode 100644 index 00000000..97973389 --- /dev/null +++ b/src/ui/audio/mobile/ArtistView.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface { +namespace Mobile { + +class ArtistView : public Wt::WContainerWidget +{ + public: + ArtistView(Wt::WContainerWidget *parent = 0); + +}; + + +} //namespace Mobile +} //namespace UserInterface + diff --git a/src/ui/audio/mobile/MobileAudio.cpp b/src/ui/audio/mobile/MobileAudio.cpp index b5d0c66a..e18087f9 100644 --- a/src/ui/audio/mobile/MobileAudio.cpp +++ b/src/ui/audio/mobile/MobileAudio.cpp @@ -17,8 +17,9 @@ * along with LMS. If not, see . */ +#include -#include +#include #include #include @@ -28,13 +29,29 @@ #include "audio/AudioPlayer.hpp" #include "LmsApplication.hpp" +#include "ArtistSearchView.hpp" +#include "PreviewSearchView.hpp" +#include "ReleaseSearch.hpp" +#include "TrackSearch.hpp" +#include "ArtistView.hpp" +#include "ReleaseView.hpp" + #include "MobileAudio.hpp" -#define SEARCH_NB_ITEMS 4 namespace UserInterface { namespace Mobile { +enum WidgetIdx +{ + WidgetIdxSearchPreview = 0, + WidgetIdxSearchArtist = 1, + WidgetIdxSearchRelease = 2, + WidgetIdxSearchTrack = 3, + WidgetIdxArtist = 4, + WidgetIdxRelease = 5, +}; + using namespace Database; static void playTrack(AudioPlayer *audioPlayer, Database::Track::id_type trackId) @@ -52,23 +69,9 @@ static void playTrack(AudioPlayer *audioPlayer, Database::Track::id_type trackId 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(); + wApp->setInternalPath("/audio/search/preview/" + stringToUTF8(text), true); } - Audio::Audio(Wt::WContainerWidget *parent) : UserInterface::Audio(parent) { @@ -76,12 +79,36 @@ Audio::Audio(Wt::WContainerWidget *parent) this->setStyleClass("container-fluid"); this->setPadding(60, Wt::Bottom); - _artistSearch = new ArtistSearch(this); - _releaseSearch = new ReleaseSearch(this); - _trackSearch = new TrackSearch(this); + Wt::WStackedWidget *stack = new Wt::WStackedWidget(this); - _trackReleaseView = new TrackReleaseView(this); - _trackReleaseView->hide(); + // Same order as WidgetIdxXXX + stack->addWidget(new PreviewSearchView()); + stack->addWidget(new ArtistSearchView()); + stack->addWidget(new ReleaseSearch()); + stack->addWidget(new TrackSearch()); + stack->addWidget(new ArtistView()); + stack->addWidget(new ReleaseView()); + + wApp->internalPathChanged().connect(std::bind([=] (std::string path) + { + // order is important + static std::map indexes = + { + { "/audio/search/preview", WidgetIdxSearchPreview}, + { "/audio/search/artist", WidgetIdxSearchArtist}, + { "/audio/search/release", WidgetIdxSearchRelease}, + { "/audio/search/track", WidgetIdxSearchTrack}, + { "/audio/artist", WidgetIdxArtist}, + { "/audio/release", WidgetIdxRelease}, + }; + + for (auto index : indexes) + { + if (wApp->internalPathMatches(index.first)) + stack->setCurrentIndex(index.second); + } + + }, std::placeholders::_1)); Wt::WTemplate* footer = new Wt::WTemplate(this); footer->setTemplateText(Wt::WString::tr("mobile-audio-footer")); @@ -89,67 +116,6 @@ Audio::Audio(Wt::WContainerWidget *parent) AudioPlayer* audioPlayer = new AudioPlayer(); footer->bindWidget("player", audioPlayer); - _artistSearch->moreArtistsSelected().connect(std::bind([=] - { - _releaseSearch->hide(); - _trackSearch->hide(); - _artistSearch->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); - }, std::placeholders::_1)); - - _releaseSearch->moreReleasesSelected().connect(std::bind([=] - { - _artistSearch->hide(); - _trackSearch->hide(); - _releaseSearch->show(); - _trackReleaseView->hide(); - })); - - _releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId) - { - _artistSearch->hide(); - _releaseSearch->hide(); - _trackSearch->hide(); - _trackReleaseView->show(); - - _trackReleaseView->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 40); - }, std::placeholders::_1)); - - _trackSearch->moreSelected().connect(std::bind([=] - { - _artistSearch->hide(); - _releaseSearch->hide(); - _trackSearch->show(); - _trackReleaseView->hide(); - })); - - _trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id) - { - playTrack(audioPlayer, id); - }, std::placeholders::_1)); - - _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); - } - } } // namespace Mobile diff --git a/src/ui/audio/mobile/MobileAudio.hpp b/src/ui/audio/mobile/MobileAudio.hpp index a62e9951..1f8ea20d 100644 --- a/src/ui/audio/mobile/MobileAudio.hpp +++ b/src/ui/audio/mobile/MobileAudio.hpp @@ -24,11 +24,6 @@ #include "audio/Audio.hpp" -#include "ArtistSearch.hpp" -#include "ReleaseSearch.hpp" -#include "TrackSearch.hpp" -#include "TrackReleaseView.hpp" - namespace UserInterface { namespace Mobile { @@ -40,11 +35,6 @@ class Audio : public UserInterface::Audio void search(std::string text); private: - - ArtistSearch* _artistSearch; - ReleaseSearch* _releaseSearch; - TrackSearch* _trackSearch; - TrackReleaseView* _trackReleaseView; }; } // namespace Mobile diff --git a/src/ui/audio/mobile/PreviewSearchView.cpp b/src/ui/audio/mobile/PreviewSearchView.cpp new file mode 100644 index 00000000..072c33ee --- /dev/null +++ b/src/ui/audio/mobile/PreviewSearchView.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "logger/Logger.hpp" +#include "SearchUtils.hpp" + +#include "ArtistSearch.hpp" +#include "ReleaseSearch.hpp" +#include "TrackSearch.hpp" + +#include "PreviewSearchView.hpp" + +namespace UserInterface { +namespace Mobile { + +#define SEARCH_NB_ITEMS 4 + +using namespace Database; + +PreviewSearchView::PreviewSearchView(Wt::WContainerWidget* parent) +{ + const std::string pathPrefix = "/audio/search/preview"; + + ArtistSearch* artistSearch = new ArtistSearch("Artists", this); + ReleaseSearch* releaseSearch = new ReleaseSearch(this); + TrackSearch* trackSearch = new TrackSearch(this); + + artistSearch->showMore().connect(std::bind([=] + { + std::string path = wApp->internalPath(); + path.replace(0, pathPrefix.length(), "/audio/search/artist"); + + wApp->setInternalPath(path, true); + })); + + releaseSearch->showMore().connect(std::bind([=] + { + std::string path = wApp->internalPath(); + path.replace(0, pathPrefix.length(), "/audio/search/release"); + + wApp->setInternalPath(path, true); + })); + + wApp->internalPathChanged().connect(std::bind([=] (std::string path) + { + if (!wApp->internalPathMatches(pathPrefix)) + return; + + std::vector keywords = searchPathToSearchKeywords(path.substr(pathPrefix.length())); + + artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS); + releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS); + trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS); + + }, std::placeholders::_1)); +} + +} // namespace Mobile +} // namespace UserInterface + diff --git a/src/ui/audio/mobile/PreviewSearchView.hpp b/src/ui/audio/mobile/PreviewSearchView.hpp new file mode 100644 index 00000000..bdcc079f --- /dev/null +++ b/src/ui/audio/mobile/PreviewSearchView.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface { +namespace Mobile { + +class PreviewSearchView : public Wt::WContainerWidget +{ + public: + + PreviewSearchView(Wt::WContainerWidget* parent = 0); + +}; + +} // namespace Mobile +} // namespace UserInterface + diff --git a/src/ui/audio/mobile/ReleaseSearch.cpp b/src/ui/audio/mobile/ReleaseSearch.cpp index fc9092e1..0ad6ff5c 100644 --- a/src/ui/audio/mobile/ReleaseSearch.cpp +++ b/src/ui/audio/mobile/ReleaseSearch.cpp @@ -98,19 +98,28 @@ ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb) Wt::WTemplate* releaseWidget = new Wt::WTemplate(this); releaseWidget->setTemplateText(Wt::WString::tr("wa-release-res")); - Wt::WImage *cover = new Wt::WImage(); + Wt::WAnchor *coverAnchor = new Wt::WAnchor(Wt::WLink(Wt:: WLink::InternalPath, "/audio/release/" + std::to_string(release.id()))); + Wt::WImage *cover = new Wt::WImage(coverAnchor); cover->setStyleClass("center-block"); 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("cover", coverAnchor); 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()); - })); + + auto artists = Artist::getByFilter(DboSession(), + SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2); + if (artists.size() == 1) + { + Wt::WAnchor *artistAnchor = new Wt::WAnchor(Wt::WLink(Wt:: WLink::InternalPath, "/audio/artist/" + std::to_string(artists.front().id()))); + Wt::WText *artist = new Wt::WText(artistAnchor); + artist->setText(Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText)); + releaseWidget->bindWidget("artist", artistAnchor); + } + else + releaseWidget->bindWidget("artist", new Wt::WText(Wt::WString::fromUTF8("Various Artists", Wt::PlainText))); } diff --git a/src/ui/audio/mobile/ReleaseSearch.hpp b/src/ui/audio/mobile/ReleaseSearch.hpp index 8c36c91c..0c179300 100644 --- a/src/ui/audio/mobile/ReleaseSearch.hpp +++ b/src/ui/audio/mobile/ReleaseSearch.hpp @@ -17,12 +17,14 @@ * along with LMS. If not, see . */ -#ifndef UI_MOBILE_RELEASE_SEARCH_HPP -#define UI_MOBILE_RELEASE_SEARCH_HPP +#pragma once #include #include +#include "database/SearchFilter.hpp" +#include "database/Types.hpp" + namespace UserInterface { namespace Mobile { @@ -53,4 +55,3 @@ class ReleaseSearch : public Wt::WContainerWidget } // namespace Mobile } // namespace UserInterface -#endif diff --git a/src/ui/audio/mobile/TrackReleaseView.cpp b/src/ui/audio/mobile/ReleaseView.cpp similarity index 88% rename from src/ui/audio/mobile/TrackReleaseView.cpp rename to src/ui/audio/mobile/ReleaseView.cpp index 73021f86..6ab32766 100644 --- a/src/ui/audio/mobile/TrackReleaseView.cpp +++ b/src/ui/audio/mobile/ReleaseView.cpp @@ -26,14 +26,14 @@ #include "utils/Utils.hpp" #include "LmsApplication.hpp" -#include "TrackReleaseView.hpp" +#include "ReleaseView.hpp" namespace UserInterface { namespace Mobile { using namespace Database; -TrackReleaseView::TrackReleaseView(Wt::WContainerWidget *parent) +ReleaseView::ReleaseView(Wt::WContainerWidget *parent) : Wt::WContainerWidget(parent) { Wt::WTemplate* wrapper = new Wt::WTemplate(this); @@ -55,13 +55,29 @@ TrackReleaseView::TrackReleaseView(Wt::WContainerWidget *parent) _showMore->bindString("text", "Tap to show more results..."); _showMore->hide(); _showMore->clicked().connect(std::bind([=] { - _sigMoreTracksSelected(); addResults(20); })); + + wApp->internalPathChanged().connect(std::bind([=] (std::string path) + { + const std::string pathPrefix = "/audio/release/"; + + if (!wApp->internalPathMatches(pathPrefix)) + return; + + std::string strId = path.substr(pathPrefix.length()); + + Release::id_type id; + if (readAs(strId, id)) + { + clear(); + search(SearchFilter::ById(SearchFilter::Field::Release, id), 20); + } + }, std::placeholders::_1)); } void -TrackReleaseView::clear() +ReleaseView::clear() { // Flush the release container _releaseContainer->clear(); @@ -73,7 +89,7 @@ TrackReleaseView::clear() } void -TrackReleaseView::search(SearchFilter filter, size_t nb) +ReleaseView::search(SearchFilter filter, size_t nb) { _filter = filter; @@ -111,7 +127,7 @@ getArtistNameFromRelease(Release::pointer release) } void -TrackReleaseView::addResults(size_t nb) +ReleaseView::addResults(size_t nb) { Wt::Dbo::Transaction transaction(DboSession()); @@ -175,8 +191,8 @@ TrackReleaseView::addResults(size_t nb) 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); + std::string format = track->getDuration().total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S"; + trackRes->bindString("time", durationToString(track->getDuration(), format), Wt::PlainText); Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText); playBtn->setStyleClass("center-block"); // TODO move to CSS? diff --git a/src/ui/audio/mobile/TrackReleaseView.hpp b/src/ui/audio/mobile/ReleaseView.hpp similarity index 89% rename from src/ui/audio/mobile/TrackReleaseView.hpp rename to src/ui/audio/mobile/ReleaseView.hpp index 5e49fec4..27cee2ac 100644 --- a/src/ui/audio/mobile/TrackReleaseView.hpp +++ b/src/ui/audio/mobile/ReleaseView.hpp @@ -17,8 +17,7 @@ * along with LMS. If not, see . */ -#ifndef UI_MOBILE_TRACK_SEARCH_HPP -#define UI_MOBILE_TRACK_SEARCH_HPP +#pragma once #include #include @@ -28,11 +27,11 @@ namespace UserInterface { namespace Mobile { -class TrackReleaseView : public Wt::WContainerWidget +class ReleaseView : public Wt::WContainerWidget { public: - TrackReleaseView(Wt::WContainerWidget *parent = 0); + ReleaseView(Wt::WContainerWidget *parent = 0); void search(Database::SearchFilter filter, size_t nb); @@ -64,4 +63,3 @@ class TrackReleaseView : public Wt::WContainerWidget } // namespace Mobile } // namespace UserInterface -#endif diff --git a/src/ui/audio/mobile/SearchUtils.hpp b/src/ui/audio/mobile/SearchUtils.hpp new file mode 100644 index 00000000..04743b15 --- /dev/null +++ b/src/ui/audio/mobile/SearchUtils.hpp @@ -0,0 +1,32 @@ + +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "utils/Utils.hpp" + +static inline std::vector searchPathToSearchKeywords(std::string path) +{ + if (!path.empty() && path[0] == '/') + path.erase(path.begin()); + + return splitString(path, " \t"); +} + diff --git a/src/ui/audio/mobile/TrackSearch.hpp b/src/ui/audio/mobile/TrackSearch.hpp index ca8c03d9..7bccbe0c 100644 --- a/src/ui/audio/mobile/TrackSearch.hpp +++ b/src/ui/audio/mobile/TrackSearch.hpp @@ -22,6 +22,9 @@ #include #include +#include "database/Types.hpp" +#include "database/SearchFilter.hpp" + namespace UserInterface { namespace Mobile { diff --git a/test/Makefile.am b/test/Makefile.am index 8025dcab..14eef55b 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,7 +1,7 @@ TESTS = database-basics database-integrity sql-query database-user -check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt test-avmetadata test-avtranscoder test-wt-audio +check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt test-avmetadata database_basics_SOURCES = \ $(srcdir)/CheckDbBasics.cpp \ @@ -83,7 +83,7 @@ test_wt_audio_SOURCES = TestWtAudio.cpp\ $(top_srcdir)/src/ui/resource/CoverResource.cpp \ $(top_srcdir)/src/ui/resource/TranscodeResource.cpp -test_wt_audio_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src $(MAGICKXX_CFLAGS) +test_wt_audio_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src -I$(top_srcdir)/src/ui $(MAGICKXX_CFLAGS) test_wt_audio_LDADD=$(MAGICKXX_LIBS) diff --git a/test/TestWt.cpp b/test/TestWt.cpp index aefbdb0a..f86d44c2 100644 --- a/test/TestWt.cpp +++ b/test/TestWt.cpp @@ -2,8 +2,62 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include + + +class ArtistView : public Wt::WContainerWidget +{ + public: + ArtistView(Wt::WContainerWidget *parent = 0) + : Wt::WContainerWidget(parent) + { + } + + void setId(int id) + { + clear(); + + Wt::WText *header = new Wt::WText("Artist view ID = " + std::to_string(id), this); + header->setInline(false); + + for (int i = id; i > 0; --i) + { + Wt::WAnchor *anchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/release/" + std::to_string(i)), this); + Wt::WText *text = new Wt::WText("Release " + std::to_string(i), anchor); + text->setInline(false); + } + } +}; + +class ReleaseView : public Wt::WContainerWidget +{ + public: + ReleaseView(Wt::WContainerWidget *parent = 0) + : Wt::WContainerWidget(parent) + { + } + + void setId(int id) + { + clear(); + Wt::WText *header = new Wt::WText("Release view ID = " + std::to_string(id), this); + header->setInline(false); + + for (int i = id; i > 0; --i) + { + Wt::WAnchor *anchor = new Wt::WAnchor(Wt::WLink(Wt::WLink::InternalPath, "/artist/" + std::to_string(i)), this); + Wt::WText *text = new Wt::WText("Artist " + std::to_string(i), anchor); + text->setInline(false); + } + } +}; class TestApplication : public Wt::WApplication { @@ -11,7 +65,70 @@ class TestApplication : public Wt::WApplication TestApplication(const Wt::WEnvironment& env) : Wt::WApplication(env) { - root()->addWidget(new Wt::WText("Hello, world!")); + + enableInternalPaths(); + + Wt::WComboBox *combo = new Wt::WComboBox(root()); + combo->addItem("Artist"); + combo->addItem("Release"); + + Wt::WLineEdit *edit = new Wt::WLineEdit("Enter id", root()); + + edit->changed().connect(std::bind([=] + { + if (combo->currentText() == "Artist") + wApp->setInternalPath("/artist/" + edit->text().toUTF8(), true); + else if (combo->currentText() == "Release") + wApp->setInternalPath("/release/" + edit->text().toUTF8(), true); + })); + + Wt::WStackedWidget *stack = new Wt::WStackedWidget(root()); + + Wt::WContainerWidget *artistContainer = new Wt::WContainerWidget(); + Wt::WContainerWidget *releaseContainer = new Wt::WContainerWidget(); + + stack->addWidget(releaseContainer); + stack->addWidget(artistContainer); + + internalPathChanged().connect(std::bind([=] (std::string path) + { + wApp->log("info") << "Path set to '" << path << "'"; + + std::vector strings; + boost::algorithm::split(strings, path, boost::is_any_of("/"), boost::token_compress_on); + + if (strings.size() != 3) + return; + + std::string view = strings[1]; + int id; + try { + id = std::stol(strings[2]); + } + catch (std::exception& e) { + return; + } + + if (view == "release") + { + releaseContainer->clear(); + ReleaseView *releaseView = new ReleaseView(releaseContainer); + releaseView->setId(id); + + stack->setCurrentIndex(0); + } + else if (view == "artist") + { + artistContainer->clear(); + ArtistView *artistView = new ArtistView(artistContainer); + artistView->setId(id); + + stack->setCurrentIndex(1); + } + + }, std::placeholders::_1)); + + setInternalPath("/main"); } };