560 lines
15 KiB
C++
560 lines
15 KiB
C++
/*
|
|
* Copyright (C) 2013 Emeric Poupon
|
|
*
|
|
* This file is part of LMS.
|
|
*
|
|
* LMS is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* LMS is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "LmsApplication.hpp"
|
|
|
|
#include <Wt/WAnchor.h>
|
|
#include <Wt/WBootstrapTheme.h>
|
|
#include <Wt/WEnvironment.h>
|
|
#include <Wt/WMenu.h>
|
|
#include <Wt/WNavigationBar.h>
|
|
#include <Wt/WPopupMenu.h>
|
|
#include <Wt/WServer.h>
|
|
#include <Wt/WStackedWidget.h>
|
|
#include <Wt/WText.h>
|
|
#include <Wt/Auth/Identity.h>
|
|
|
|
#include "config/config.h"
|
|
#include "utils/Logger.hpp"
|
|
#include "utils/Utils.hpp"
|
|
|
|
#include "explore/Explore.hpp"
|
|
#include "MediaPlayer.hpp"
|
|
#include "PlayHistoryView.hpp"
|
|
#include "PlayQueueView.hpp"
|
|
#include "SettingsView.hpp"
|
|
|
|
#include "admin/InitWizardView.hpp"
|
|
#include "admin/DatabaseSettingsView.hpp"
|
|
#include "admin/UserView.hpp"
|
|
#include "admin/UsersView.hpp"
|
|
|
|
#include "resource/ImageResource.hpp"
|
|
#include "resource/TranscodeResource.hpp"
|
|
|
|
namespace UserInterface {
|
|
|
|
std::unique_ptr<Wt::WApplication>
|
|
LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner)
|
|
{
|
|
/*
|
|
* You could read information from the environment to decide whether
|
|
* the user has permission to start a new application
|
|
*/
|
|
return std::make_unique<LmsApplication>(env, connectionPool, appGroups, scanner);
|
|
}
|
|
|
|
LmsApplication*
|
|
LmsApplication::instance()
|
|
{
|
|
return reinterpret_cast<LmsApplication*>(Wt::WApplication::instance());
|
|
}
|
|
|
|
/*
|
|
* The env argument contains information about the new session, and
|
|
* the initial request. It must be passed to the Wt::WApplication
|
|
* constructor so it is typically also an argument for your custom
|
|
* application constructor.
|
|
*/
|
|
LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner)
|
|
: Wt::WApplication(env),
|
|
_db(connectionPool),
|
|
_appGroups(appGroups),
|
|
_scanner(scanner),
|
|
_imageResource(nullptr),
|
|
_transcodeResource(nullptr)
|
|
{
|
|
auto bootstrapTheme = std::make_unique<Wt::WBootstrapTheme>();
|
|
bootstrapTheme->setVersion(Wt::BootstrapVersion::v3);
|
|
bootstrapTheme->setResponsive(true);
|
|
setTheme(std::move(bootstrapTheme));
|
|
|
|
useStyleSheet("css/lms.css");
|
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
|
|
|
// Add a resource bundle
|
|
messageResourceBundle().use(appRoot() + "admin-database");
|
|
messageResourceBundle().use(appRoot() + "admin-user");
|
|
messageResourceBundle().use(appRoot() + "admin-users");
|
|
messageResourceBundle().use(appRoot() + "admin-initwizard");
|
|
messageResourceBundle().use(appRoot() + "artist");
|
|
messageResourceBundle().use(appRoot() + "artists");
|
|
messageResourceBundle().use(appRoot() + "artistsinfo");
|
|
messageResourceBundle().use(appRoot() + "explore");
|
|
messageResourceBundle().use(appRoot() + "login");
|
|
messageResourceBundle().use(appRoot() + "mediaplayer");
|
|
messageResourceBundle().use(appRoot() + "messages");
|
|
messageResourceBundle().use(appRoot() + "playqueue");
|
|
messageResourceBundle().use(appRoot() + "playhistory");
|
|
messageResourceBundle().use(appRoot() + "release");
|
|
messageResourceBundle().use(appRoot() + "releases");
|
|
messageResourceBundle().use(appRoot() + "releasesinfo");
|
|
messageResourceBundle().use(appRoot() + "settings");
|
|
messageResourceBundle().use(appRoot() + "templates");
|
|
messageResourceBundle().use(appRoot() + "tracks");
|
|
messageResourceBundle().use(appRoot() + "tracksinfo");
|
|
|
|
// Require js here to avoid async problems
|
|
requireJQuery("/js/jquery-1.10.2.min.js");
|
|
require("/js/mediaplayer.js");
|
|
require("/js/bootstrap-notify.js");
|
|
|
|
setTitle("LMS");
|
|
|
|
// If here is no account in the database, launch the first connection wizard
|
|
bool firstConnection;
|
|
{
|
|
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
|
|
|
|
firstConnection = (Database::User::getAll(LmsApp->getDboSession()).size() == 0);
|
|
}
|
|
|
|
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection;
|
|
|
|
if (firstConnection)
|
|
{
|
|
root()->addWidget(std::make_unique<InitWizardView>());
|
|
}
|
|
else
|
|
{
|
|
LmsApp->getDb().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
|
|
_auth = root()->addNew<Auth>();
|
|
}
|
|
}
|
|
|
|
void
|
|
LmsApplication::finalize()
|
|
{
|
|
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
|
|
|
|
getApplicationGroup().postOthers([info]
|
|
{
|
|
LmsApp->getGroupEvents().appClosed(info);
|
|
});
|
|
|
|
getApplicationGroup().leave();
|
|
|
|
preQuit().emit();
|
|
}
|
|
|
|
Wt::WLink
|
|
LmsApplication::createArtistLink(Database::Artist::pointer artist)
|
|
{
|
|
return Wt::WLink(Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id()));
|
|
}
|
|
|
|
std::unique_ptr<Wt::WAnchor>
|
|
LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText)
|
|
{
|
|
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
|
|
|
|
if (addText)
|
|
{
|
|
res->setTextFormat(Wt::TextFormat::Plain);
|
|
res->setText(Wt::WString::fromUTF8(artist->getName()));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
Wt::WLink
|
|
LmsApplication::createReleaseLink(Database::Release::pointer release)
|
|
{
|
|
return Wt::WLink(Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id()));
|
|
}
|
|
|
|
std::unique_ptr<Wt::WAnchor>
|
|
LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool addText)
|
|
{
|
|
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
|
|
|
|
if (addText)
|
|
{
|
|
res->setTextFormat(Wt::TextFormat::Plain);
|
|
res->setText(Wt::WString::fromUTF8(release->getName()));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::unique_ptr<Wt::WTemplate>
|
|
LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete)
|
|
{
|
|
std::string styleClass = "Lms-cluster-type-" + std::to_string(cluster->getType().id() % 10);
|
|
|
|
auto res = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.template.cluster-entry").arg(styleClass));
|
|
res->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
|
|
|
res->bindString("name", Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::Plain);
|
|
res->setCondition("if-can-delete", canDelete);
|
|
|
|
res->setStyleClass("Lms-cluster");
|
|
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
|
|
res->setInline(true);
|
|
|
|
return res;
|
|
}
|
|
|
|
void
|
|
LmsApplication::goHome()
|
|
{
|
|
setInternalPath("/artists", true);
|
|
}
|
|
|
|
void
|
|
LmsApplication::goHomeAndQuit()
|
|
{
|
|
setConfirmCloseMessage("");
|
|
WApplication::quit("");
|
|
redirect("/");
|
|
}
|
|
|
|
enum IdxRoot
|
|
{
|
|
IdxExplore = 0,
|
|
IdxPlayQueue,
|
|
IdxPlayHistory,
|
|
IdxSettings,
|
|
IdxAdminDatabase,
|
|
IdxAdminUsers,
|
|
IdxAdminUser,
|
|
};
|
|
|
|
static void
|
|
handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
|
{
|
|
static const struct
|
|
{
|
|
std::string path;
|
|
int index;
|
|
bool admin;
|
|
} views[] =
|
|
{
|
|
{ "/artists", IdxExplore, false },
|
|
{ "/artist", IdxExplore, false },
|
|
{ "/releases", IdxExplore, false },
|
|
{ "/release", IdxExplore, false },
|
|
{ "/tracks", IdxExplore, false },
|
|
{ "/playqueue", IdxPlayQueue, false },
|
|
{ "/playhistory", IdxPlayHistory, false },
|
|
{ "/settings", IdxSettings, false },
|
|
{ "/admin/database", IdxAdminDatabase, true },
|
|
{ "/admin/users", IdxAdminUsers, true },
|
|
{ "/admin/user", IdxAdminUser, true },
|
|
};
|
|
|
|
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
|
|
|
for (auto& view : views)
|
|
{
|
|
if (wApp->internalPathMatches(view.path))
|
|
{
|
|
if (view.admin && !isAdmin)
|
|
break;
|
|
|
|
stack->setCurrentIndex(view.index);
|
|
return;
|
|
}
|
|
}
|
|
|
|
wApp->setInternalPath("/artists", true);
|
|
}
|
|
|
|
LmsApplicationGroup&
|
|
LmsApplication::getApplicationGroup()
|
|
{
|
|
return _appGroups.get(_userIdentity);
|
|
}
|
|
|
|
void
|
|
LmsApplication::handleAuthEvent()
|
|
{
|
|
try
|
|
{
|
|
if (!getDb().getLogin().loggedIn())
|
|
{
|
|
LMS_LOG(UI, INFO) << "User '" << _userIdentity << " 'logged out, session = " << sessionId();
|
|
|
|
goHomeAndQuit();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
_userIdentity = getAuthUser().identity(Wt::Auth::Identity::LoginName);
|
|
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
|
|
|
|
LMS_LOG(UI, INFO) << "User '" << _userIdentity << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", session = " << sessionId();
|
|
getApplicationGroup().join(info);
|
|
|
|
getApplicationGroup().postOthers([info]
|
|
{
|
|
LmsApp->getGroupEvents().appOpen(info);
|
|
});
|
|
|
|
createHome();
|
|
}
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
LMS_LOG(UI, ERROR) << "Error while handling auth event: " << e.what();
|
|
throw LmsException("Internal error"); // Do not put details here at it appears on the user rendered html
|
|
}
|
|
}
|
|
|
|
void
|
|
LmsApplication::createHome()
|
|
{
|
|
// Handle Media Scanner events and other session events
|
|
enableUpdates(true);
|
|
|
|
{
|
|
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
|
|
_isAdmin = LmsApp->getUser()->isAdmin();
|
|
}
|
|
|
|
_imageResource = std::make_shared<ImageResource>(_db);
|
|
_transcodeResource = std::make_shared<TranscodeResource>(_db);
|
|
|
|
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
|
|
|
|
Wt::WTemplate* main = root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")));
|
|
|
|
// Navbar
|
|
Wt::WNavigationBar* navbar = main->bindNew<Wt::WNavigationBar>("navbar-top");
|
|
navbar->setTitle("LMS", Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
|
|
navbar->setResponsive(true);
|
|
|
|
Wt::WMenu* menu = navbar->addMenu(std::make_unique<Wt::WMenu>());
|
|
{
|
|
auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
{
|
|
auto menuItem = menu->insertItem(1, Wt::WString::tr("Lms.Explore.releases"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/releases"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
{
|
|
auto menuItem = menu->insertItem(2, Wt::WString::tr("Lms.Explore.tracks"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/tracks"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
{
|
|
auto menuItem = menu->insertItem(3, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playqueue"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
{
|
|
auto menuItem = menu->insertItem(4, Wt::WString::tr("Lms.PlayHistory.playhistory"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playhistory"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
|
|
Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique<Wt::WMenu>(), Wt::AlignmentFlag::Right);
|
|
std::size_t itemCounter = 0;
|
|
if (_isAdmin)
|
|
{
|
|
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration"));
|
|
menuItem->setSelectable(false);
|
|
|
|
auto admin = std::make_unique<Wt::WPopupMenu>();
|
|
auto dbSettings = admin->insertItem(0, Wt::WString::tr("Lms.Admin.Database.database"));
|
|
dbSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/database"));
|
|
dbSettings->setSelectable(false);
|
|
|
|
auto usersSettings = admin->insertItem(1, Wt::WString::tr("Lms.Admin.Users.users"));
|
|
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
|
|
usersSettings->setSelectable(false);
|
|
|
|
menuItem->setMenu(std::move(admin));
|
|
}
|
|
|
|
{
|
|
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.Settings.settings"));
|
|
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/settings"));
|
|
menuItem->setSelectable(false);
|
|
}
|
|
{
|
|
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout"));
|
|
menuItem->setSelectable(true);
|
|
menuItem->triggered().connect(std::bind([=]
|
|
{
|
|
setConfirmCloseMessage("");
|
|
_auth->logout();
|
|
}));
|
|
}
|
|
|
|
// Contents
|
|
// Order is important in mainStack, see IdxRoot!
|
|
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
|
|
|
|
Explore* explore = mainStack->addNew<Explore>();
|
|
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
|
|
PlayHistory* playhistory = mainStack->addNew<PlayHistory>();
|
|
mainStack->addNew<SettingsView>();
|
|
|
|
// Admin stuff
|
|
if (_isAdmin)
|
|
{
|
|
mainStack->addNew<DatabaseSettingsView>();
|
|
mainStack->addNew<UsersView>();
|
|
mainStack->addNew<UserView>();
|
|
}
|
|
|
|
explore->tracksAdd.connect([=] (std::vector<Database::Track::pointer> tracks)
|
|
{
|
|
playqueue->addTracks(tracks);
|
|
});
|
|
|
|
explore->tracksPlay.connect([=] (std::vector<Database::Track::pointer> tracks)
|
|
{
|
|
playqueue->playTracks(tracks);
|
|
});
|
|
|
|
|
|
// MediaPlayer
|
|
MediaPlayer* player = main->bindNew<MediaPlayer>("player");
|
|
|
|
// Events from MediaPlayer
|
|
player->playNext.connect([=]
|
|
{
|
|
playqueue->playNext();
|
|
});
|
|
player->playPrevious.connect([=]
|
|
{
|
|
playqueue->playPrevious();
|
|
});
|
|
player->playbackEnded.connect([=]
|
|
{
|
|
playqueue->playNext();
|
|
});
|
|
|
|
// Events from the PlayQueue
|
|
playqueue->playTrack.connect(playhistory, &PlayHistory::addTrack);
|
|
playqueue->playTrack.connect(explore, &Explore::handleTrackPlayed);
|
|
playqueue->playTrack.connect(player, &MediaPlayer::playTrack);
|
|
|
|
playqueue->playbackStop.connect(player, &MediaPlayer::stop);
|
|
|
|
// Events from MediaScanner
|
|
std::string sessionId = LmsApp->sessionId();
|
|
_scanner.scanComplete().connect([=] (Scanner::MediaScanner::Stats stats)
|
|
{
|
|
Wt::WServer::instance()->post(sessionId, [=]
|
|
{
|
|
bool changes = false;
|
|
|
|
if (_isAdmin)
|
|
{
|
|
notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
|
.arg(static_cast<unsigned>(stats.nbFiles()))
|
|
.arg(static_cast<unsigned>(stats.additions))
|
|
.arg(static_cast<unsigned>(stats.updates))
|
|
.arg(static_cast<unsigned>(stats.deletions))
|
|
.arg(static_cast<unsigned>(stats.nbDuplicates()))
|
|
.arg(static_cast<unsigned>(stats.nbErrors())));
|
|
changes = true;
|
|
}
|
|
|
|
if (stats.nbChanges() > 0)
|
|
{
|
|
explore->handleDbChanged();
|
|
changes = true;
|
|
}
|
|
|
|
if (changes)
|
|
triggerUpdate();
|
|
});
|
|
});
|
|
|
|
// Events from Application group
|
|
_groupEvents.appOpen.connect([=] (LmsApplicationInfo info)
|
|
{
|
|
// Only one active session by user
|
|
if (!LmsApp->getUser()->isDemo())
|
|
{
|
|
setConfirmCloseMessage("");
|
|
quit(Wt::WString::tr("Lms.quit-other-session"));
|
|
}
|
|
});
|
|
|
|
_groupEvents.appClosed.connect([=] (LmsApplicationInfo info)
|
|
{
|
|
;
|
|
});
|
|
|
|
internalPathChanged().connect(std::bind([=]
|
|
{
|
|
handlePathChange(mainStack, _isAdmin);
|
|
}));
|
|
|
|
handlePathChange(mainStack, _isAdmin);
|
|
}
|
|
|
|
void
|
|
LmsApplication::notify(const Wt::WEvent& event)
|
|
{
|
|
try
|
|
{
|
|
WApplication::notify(event);
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
|
|
throw LmsException("Internal error"); // Do not put details here at it appears on the user rendered html
|
|
}
|
|
}
|
|
|
|
static std::string msgTypeToString(MsgType type)
|
|
{
|
|
switch(type)
|
|
{
|
|
case MsgType::Success: return "success";
|
|
case MsgType::Info: return "info";
|
|
case MsgType::Warning: return "warning";
|
|
case MsgType::Danger: return "danger";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
void
|
|
LmsApplication::notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration)
|
|
{
|
|
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' of type '" << msgTypeToString(type);
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "$.notify({"
|
|
"message: '" << message.toUTF8() << "'"
|
|
"},{"
|
|
"type: '" << msgTypeToString(type) << "',"
|
|
"placement: {from: 'top', align: 'center'},"
|
|
"timer: 250,"
|
|
"delay: " << duration.count() << ""
|
|
"});";
|
|
|
|
LmsApp->doJavaScript(oss.str());
|
|
}
|
|
|
|
|
|
} // namespace UserInterface
|
|
|
|
|