diff --git a/Makefile.am b/Makefile.am index 7153c82b..95bf74bc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -24,6 +24,7 @@ lms_approot_DATA = \ approot/playlist.xml \ approot/releases.xml \ approot/release.xml \ + approot/settings.xml \ approot/templates.xml \ approot/tracks.xml diff --git a/approot/messages.xml b/approot/messages.xml index 38d2396f..6bf0010a 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -17,4 +17,19 @@ Value Show more + +Settings +Database +Media root directory +Update period +Update start time +Never +Daily +Weekly +Monthly +Not a directory +Apply +Discard + + diff --git a/approot/settings.xml b/approot/settings.xml new file mode 100644 index 00000000..75f4c661 --- /dev/null +++ b/approot/settings.xml @@ -0,0 +1,54 @@ + + + + + + + ${tr:msg-settings-database} + + + + + ${tr:msg-settings-media-directory} + + + ${media-directory} + + + ${media-directory-info} + + + + + + ${tr:msg-settings-update-period} + + + ${update-period} + + + ${update-period-info} + + + + + + ${tr:msg-settings-update-start-time} + + + ${update-start-time} + + + ${update-start-time-info} + + + + + + ${apply-btn class="btn-primary"} ${discard-btn} + + + + + + diff --git a/src/Makefile.am b/src/Makefile.am index 7842e2dd..ec57c4a9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -39,6 +39,7 @@ lms_SOURCES = \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ $(srcdir)/ui/resource/ImageResource.cpp \ $(srcdir)/ui/resource/TranscodeResource.cpp \ + $(srcdir)/ui/settings/DatabaseView.cpp \ $(srcdir)/utils/Config.cpp \ $(srcdir)/utils/Logger.cpp \ $(srcdir)/utils/Path.cpp \ diff --git a/src/main/main.cpp b/src/main/main.cpp index c2b24416..2901d976 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -127,10 +127,6 @@ int main(int argc, char* argv[]) Scanner::MediaScanner scanner(*connectionPool); - // Instanciate the updater's event handler. Order is important -// dbUpdater.registerEventHandler(std::make_shared()); -// dbUpdater.registerEventHandler(std::make_shared()); - // bind entry point server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create, _1, boost::ref(*connectionPool))); diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp index 60f19f55..7f1239d8 100644 --- a/src/scanner/MediaScanner.cpp +++ b/src/scanner/MediaScanner.cpp @@ -107,6 +107,28 @@ namespace Scanner { using namespace Database; +UpdatePeriod +getUpdatePeriod(Wt::Dbo::Session& session) +{ + return static_cast(Setting::getInt(session, "update_period", static_cast(UpdatePeriod::Never))); +} + +void +setUpdatePeriod(Wt::Dbo::Session& session, UpdatePeriod updatePeriod) +{ + Setting::setInt(session, "update_period", static_cast(updatePeriod)); +} + +boost::posix_time::time_duration getUpdateStartTime(Wt::Dbo::Session& session) +{ + return Setting::getDuration(session, "update_start_time"); +} + +void setUpdateStartTime(Wt::Dbo::Session& session, boost::posix_time::time_duration startTime) +{ + Setting::setDuration(session, "update_start_time", startTime); +} + MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool) : _running(false), _scheduleTimer(_ioService), @@ -159,31 +181,35 @@ MediaScanner::processNextJob(void) else { boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - boost::posix_time::time_duration startTime = Setting::getDuration(_db.getSession(), "update_start_time"); + boost::posix_time::time_duration startTime = getUpdateStartTime(_db.getSession()); boost::gregorian::date nextScanDate; - std::string updatePeriod = Setting::getString(_db.getSession(), "update_period", "never"); - if (updatePeriod == "daily") + switch ( getUpdatePeriod(_db.getSession()) ) { - if (now.time_of_day() < startTime) - nextScanDate = now.date(); - else - nextScanDate = getNextDay(now.date()); - } - else if (updatePeriod == "weekly") - { - if (now.time_of_day() < startTime && now.date().day_of_week() == 1) - nextScanDate = now.date(); - else - nextScanDate = getNextMonday(now.date()); - } - else if (updatePeriod == "monthly") - { - if (now.time_of_day() < startTime && now.date().day() == 1) - nextScanDate = now.date(); - else - nextScanDate = getNextFirstOfMonth(now.date()); + case UpdatePeriod::Daily: + if (now.time_of_day() < startTime) + nextScanDate = now.date(); + else + nextScanDate = getNextDay(now.date()); + break; + + case UpdatePeriod::Weekly: + if (now.time_of_day() < startTime && now.date().day_of_week() == 1) + nextScanDate = now.date(); + else + nextScanDate = getNextMonday(now.date()); + break; + + case UpdatePeriod::Monthly: + if (now.time_of_day() < startTime && now.date().day() == 1) + nextScanDate = now.date(); + else + nextScanDate = getNextFirstOfMonth(now.date()); + break; + + case UpdatePeriod::Never: + break; } if (!nextScanDate.is_special()) diff --git a/src/scanner/MediaScanner.hpp b/src/scanner/MediaScanner.hpp index fd8d1e5f..6d319f0b 100644 --- a/src/scanner/MediaScanner.hpp +++ b/src/scanner/MediaScanner.hpp @@ -30,6 +30,18 @@ namespace Scanner { +enum class UpdatePeriod { + Never = 0, + Daily, + Weekly, + Monthly +}; + +UpdatePeriod getUpdatePeriod(Wt::Dbo::Session& session); +void setUpdatePeriod(Wt::Dbo::Session& session, UpdatePeriod updatePeriod); + +boost::posix_time::time_duration getUpdateStartTime(Wt::Dbo::Session& session); +void setUpdateStartTime(Wt::Dbo::Session& session, boost::posix_time::time_duration); class MediaScanner { diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index ba1997ab..dcfd42ff 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "config/config.h" @@ -34,6 +35,8 @@ #include "MediaPlayer.hpp" #include "PlaylistView.hpp" +#include "settings/DatabaseView.hpp" + #include "LmsApplication.hpp" namespace skeletons { @@ -86,6 +89,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti messageResourceBundle().use(appRoot() + "playlist"); messageResourceBundle().use(appRoot() + "release"); messageResourceBundle().use(appRoot() + "releases"); + messageResourceBundle().use(appRoot() + "settings"); messageResourceBundle().use(appRoot() + "tracks"); messageResourceBundle().use(appRoot() + "templates"); @@ -179,7 +183,7 @@ enum IdxRoot IdxHome = 0, IdxExplore, IdxPlaylist, - IdxSettings, + IdxSettingsDatabase, }; @@ -196,7 +200,7 @@ handlePathChange(Wt::WStackedWidget* stack) { "/release", IdxExplore }, { "/tracks", IdxExplore }, { "/playlist", IdxPlaylist }, - { "/settings", IdxSettings }, + { "/settings/database", IdxSettingsDatabase }, }; LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; @@ -237,7 +241,6 @@ LmsApplication::handleAuthEvent(void) auto navbar = new Wt::WNavigationBar(); navbar->setTitle("LMS", Wt::WLink(Wt::WLink::InternalPath, "/home")); navbar->setResponsive(true); -// navbar->setStyleClass("main-nav"); main->bindWidget("navbar-top", navbar); @@ -262,6 +265,18 @@ LmsApplication::handleAuthEvent(void) menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/playlist")); menuItem->setSelectable(false); } + { + auto menuItem = menu->insertItem(4, Wt::WString::tr("msg-settings")); + menuItem->setSelectable(false); + + Wt::WPopupMenu *settings = new Wt::WPopupMenu(); + auto dbSettings = settings->insertItem(0, Wt::WString::tr("msg-settings-database")); + dbSettings->setLink(Wt::WLink(Wt::WLink::InternalPath, "/settings/database")); + dbSettings->setSelectable(false); + + menuItem->setMenu(settings); + } + navbar->addMenu(menu); // Contents @@ -275,7 +290,9 @@ LmsApplication::handleAuthEvent(void) auto playlist = new Playlist(); mainStack->addWidget(playlist); - mainStack->addWidget(new Wt::WText("SETTINGS")); + + auto databaseSettings = new Settings::DatabaseView(); + mainStack->addWidget(databaseSettings); explore->tracksAdd.connect(std::bind([=] (std::vector tracks) { diff --git a/src/ui/common/DirectoryValidator.cpp b/src/ui/common/DirectoryValidator.cpp index c0093f1d..cf6c16fd 100644 --- a/src/ui/common/DirectoryValidator.cpp +++ b/src/ui/common/DirectoryValidator.cpp @@ -38,13 +38,14 @@ DirectoryValidator::validate(const Wt::WString& input) const boost::filesystem::path p(input.toUTF8()); boost::system::error_code ec; + // TODO check rights bool res = boost::filesystem::is_directory(p, ec); if (ec) - return Wt::WValidator::Result(Wt::WValidator::Invalid, ec.message()); + return Wt::WValidator::Result(Wt::WValidator::Invalid, ec.message()); // TODO translate common errors else if (res) return Wt::WValidator::Result(Wt::WValidator::Valid); else - return Wt::WValidator::Result(Wt::WValidator::Invalid, "Not a directory"); + return Wt::WValidator::Result(Wt::WValidator::Invalid, Wt::WString::tr("msg-error-not-a-directory")); } diff --git a/src/ui/settings/DatabaseView.cpp b/src/ui/settings/DatabaseView.cpp new file mode 100644 index 00000000..355b3b25 --- /dev/null +++ b/src/ui/settings/DatabaseView.cpp @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2018 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "common/DirectoryValidator.hpp" +#include "database/MediaDirectory.hpp" +#include "scanner/MediaScanner.hpp" +#include "utils/Logger.hpp" + +#include "LmsApplication.hpp" + +#include "DatabaseView.hpp" + +namespace UserInterface { +namespace Settings { + +using namespace Database; + +class DatabaseModel : public Wt::WFormModel +{ + public: + // Associate each field with a unique string literal. + static const Field MediaDirectoryField; + static const Field UpdatePeriodField; + static const Field UpdateStartTimeField; + + DatabaseModel(Wt::WObject *parent = 0) + : Wt::WFormModel(parent) + { + initializeModels(); + + addField(MediaDirectoryField); + addField(UpdatePeriodField); + addField(UpdateStartTimeField); + + DirectoryValidator* dirValidator = new DirectoryValidator(); + dirValidator->setMandatory(true); + setValidator(MediaDirectoryField, dirValidator); + + setValidator(UpdatePeriodField, createMandatoryValidator()); + setValidator(UpdateStartTimeField, createMandatoryValidator()); + + // populate the model with initial data + loadData(); + } + + Wt::WAbstractItemModel *updatePeriodModel() { return _updatePeriodModel; } + Wt::WAbstractItemModel *updateStartTimeModel() { return _updateStartTimeModel; } + + void loadData() + { + using namespace Database; + + Wt::Dbo::Transaction transaction(DboSession()); + + std::vector mediaDirectories = MediaDirectory::getAll(DboSession()); + if (!mediaDirectories.empty()) + setValue(MediaDirectoryField, mediaDirectories.front()->getPath().string()); + + auto periodRow = getUpdatePeriodModelRow( Scanner::getUpdatePeriod(DboSession()) ); + if (periodRow) + setValue(UpdatePeriodField, updatePeriodString(*periodRow)); + + auto startTimeRow = getUpdateStartTimeModelRow( Scanner::getUpdateStartTime(DboSession()) ); + if (startTimeRow) + setValue(UpdateStartTimeField, updateStartTimeString(*startTimeRow) ); + } + + void saveData() + { + Wt::Dbo::Transaction transaction(DboSession()); + + MediaDirectory::eraseAll(DboSession()); + MediaDirectory::create(DboSession(), boost::any_cast(value(MediaDirectoryField)).toUTF8()); + + auto updatePeriodRow = getUpdatePeriodModelRow( boost::any_cast(value(UpdatePeriodField))); + assert(updatePeriodRow); + Scanner::setUpdatePeriod(DboSession(), updatePeriod(*updatePeriodRow)); + + auto startTimeRow = getUpdateStartTimeModelRow( boost::any_cast(value(UpdateStartTimeField))); + assert(startTimeRow); + Scanner::setUpdateStartTime(DboSession(), updateStartTime(*startTimeRow)); + } + + boost::optional getUpdatePeriodModelRow(Wt::WString value) + { + for (int i = 0; i < _updatePeriodModel->rowCount(); ++i) + { + if (updatePeriodString(i) == value) + return i; + } + + return boost::none; + } + + boost::optional getUpdatePeriodModelRow(Scanner::UpdatePeriod period) + { + for (int i = 0; i < _updatePeriodModel->rowCount(); ++i) + { + if (updatePeriod(i) == period) + return i; + } + + return boost::none; + } + + Scanner::UpdatePeriod updatePeriod(int row) + { + return boost::any_cast + (_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::UserRole)); + } + + Wt::WString updatePeriodString(int row) + { + return boost::any_cast + (_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::DisplayRole)); + } + + + boost::optional getUpdateStartTimeModelRow(Wt::WString value) + { + for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i) + { + if (updateStartTimeString(i) == value) + return i; + } + + return boost::none; + } + + boost::optional getUpdateStartTimeModelRow(boost::posix_time::time_duration startTime) + { + for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i) + { + if (updateStartTime(i) == startTime) + return i; + } + + return boost::none; + } + + boost::posix_time::time_duration updateStartTime(int row) + { + return boost::any_cast + (_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::UserRole)); + } + + Wt::WString updateStartTimeString(int row) + { + return boost::any_cast + (_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::DisplayRole)); + } + + + private: + + void initializeModels() { + + _updatePeriodModel = new Wt::WStringListModel(this); + + _updatePeriodModel->addString(Wt::WString::tr("msg-update-period-never")); + _updatePeriodModel->setData(0, 0, Scanner::UpdatePeriod::Never, Wt::UserRole); + + _updatePeriodModel->addString(Wt::WString::tr("msg-update-period-daily")); + _updatePeriodModel->setData(1, 0, Scanner::UpdatePeriod::Daily, Wt::UserRole); + + _updatePeriodModel->addString(Wt::WString::tr("msg-update-period-weekly")); + _updatePeriodModel->setData(2, 0, Scanner::UpdatePeriod::Weekly, Wt::UserRole); + + _updatePeriodModel->addString(Wt::WString::tr("msg-update-period-monthly")); + _updatePeriodModel->setData(3, 0, Scanner::UpdatePeriod::Monthly, Wt::UserRole); + + _updateStartTimeModel = new Wt::WStringListModel(this); + + for (std::size_t i = 0; i < 24; ++i) + { + boost::posix_time::time_duration dur = boost::posix_time::hours(i); + + boost::posix_time::time_facet* facet = new boost::posix_time::time_facet("%H:%M"); + facet->time_duration_format("%H:%M"); + + std::ostringstream oss; + oss.imbue(std::locale(oss.getloc(), facet)); + + oss << dur; + + _updateStartTimeModel->addString( oss.str() ); + _updateStartTimeModel->setData(i, 0, dur, Wt::UserRole); + } + + } + + Wt::WValidator *createMandatoryValidator() + { + Wt::WValidator* v = new Wt::WValidator(); + v->setMandatory(true); + return v; + } + + Wt::WStringListModel* _updatePeriodModel; + Wt::WStringListModel* _updateStartTimeModel; + +}; + +const Wt::WFormModel::Field DatabaseModel::MediaDirectoryField = "media-directory"; +const Wt::WFormModel::Field DatabaseModel::UpdatePeriodField = "update-period"; +const Wt::WFormModel::Field DatabaseModel::UpdateStartTimeField = "update-start-time"; + + +DatabaseView::DatabaseView(Wt::WContainerWidget *parent) +: Wt::WTemplateFormView(parent) +{ + _model = new DatabaseModel(this); + + setTemplateText(tr("template-settings-database")); + addFunction("tr", &WTemplate::Functions::tr); + addFunction("id", &WTemplate::Functions::id); + + // Media Directory + Wt::WLineEdit *mediaDirectoryEdit = new Wt::WLineEdit(); + setFormWidget(DatabaseModel::MediaDirectoryField, mediaDirectoryEdit); + + // Update Period + Wt::WComboBox *updatePeriodCB = new Wt::WComboBox(); + setFormWidget(DatabaseModel::UpdatePeriodField, updatePeriodCB); + updatePeriodCB->setModel(_model->updatePeriodModel()); + + // Update Start Time + Wt::WComboBox *updateStartTimeCB = new Wt::WComboBox(); + setFormWidget(DatabaseModel::UpdateStartTimeField, updateStartTimeCB); + updateStartTimeCB->setModel(_model->updateStartTimeModel()); + + // Buttons + + Wt::WPushButton *saveBtn = new Wt::WPushButton(Wt::WString::tr("msg-btn-apply")); + bindWidget("apply-btn", saveBtn); + saveBtn->setStyleClass("btn-primary"); + + Wt::WPushButton *discardBtn = new Wt::WPushButton(Wt::WString::tr("msg-btn-discard")); + bindWidget("discard-btn", discardBtn); + + saveBtn->clicked().connect(this, &DatabaseView::processSave); + discardBtn->clicked().connect(this, &DatabaseView::processDiscard); + + updateView(_model); +} + +void +DatabaseView::processDiscard() +{ + _model->loadData(); + _model->validate(); + updateView(_model); +} + +void +DatabaseView::processSave() +{ + updateModel(_model); + + if (_model->validate()) { + _model->saveData(); + +// _sigChanged.emit(); + } + // Udate the view: Delete any validation message in the view, etc. + updateView(_model); +} + + +} // namespace Settings +} // namespace UserInterface + + diff --git a/src/ui/settings/DatabaseView.hpp b/src/ui/settings/DatabaseView.hpp new file mode 100644 index 00000000..ad82178c --- /dev/null +++ b/src/ui/settings/DatabaseView.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace UserInterface { +namespace Settings { + +class DatabaseModel; + +class DatabaseView : public Wt::WTemplateFormView +{ + public: + DatabaseView(Wt::WContainerWidget *parent = 0); + + Wt::Signal& changed() { return _sigChanged; } + + private: + + Wt::Signal _sigChanged; + + void processSave(); + void processDiscard(); + void processImmediateScan(); + + Wt::WText *_applyInfo; + DatabaseModel *_model; +}; + + +} // namespace Settings +} // namespace UserInterface +