diff --git a/Makefile.am b/Makefile.am index 9b4a576f..0c4a11d0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -29,6 +29,7 @@ lms_approot_DATA = \ approot/playqueue.xml \ approot/release.xml \ approot/releases.xml \ + approot/settings.xml \ approot/templates.xml \ approot/tracks.xml diff --git a/approot/messages.xml b/approot/messages.xml index 6d8f9fb0..9d2fe7bf 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -13,9 +13,9 @@ Not a directory Password Confirm password +Passwords don't match Play Queue Are you sure? -Settings Daily @@ -54,9 +54,16 @@ Value Various artists - - - +Account +Audio +Auto +Bitrate +Encoding +MP3 +OGA +Settings +WEBMA +New settings saved! diff --git a/approot/settings.xml b/approot/settings.xml new file mode 100644 index 00000000..32e87bfe --- /dev/null +++ b/approot/settings.xml @@ -0,0 +1,65 @@ + + + + + + + ${tr:Lms.Settings.audio} + + + + ${tr:Lms.Settings.bitrate} + + + ${bitrate} + + + ${bitrate-info} + + + + + ${tr:Lms.Settings.encoding} + + + ${encoding} + + + ${encoding-info} + + + + ${tr:Lms.Settings.account} + + + + ${tr:Lms.password} + + + ${password} + + + ${password-info} + + + + + ${tr:Lms.password-confirm} + + + ${password-confirm} + + + ${password-confirm-info} + + + + + + ${apply-btn class="btn-primary"} ${discard-btn} + + + + + + diff --git a/src/Makefile.am b/src/Makefile.am index 6289feca..66a1efd3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -21,6 +21,17 @@ lms_SOURCES = \ $(srcdir)/metadata/AvFormat.cpp \ $(srcdir)/metadata/TagLibParser.cpp \ $(srcdir)/scanner/MediaScanner.cpp \ + $(srcdir)/ui/Auth.cpp \ + $(srcdir)/ui/HomeView.cpp \ + $(srcdir)/ui/LmsApplication.cpp \ + $(srcdir)/ui/MediaPlayer.cpp \ + $(srcdir)/ui/PlayQueueView.cpp \ + $(srcdir)/ui/SettingsView.cpp \ + $(srcdir)/ui/admin/DatabaseSettingsView.cpp \ + $(srcdir)/ui/admin/AdminWizardView.cpp \ + $(srcdir)/ui/common/InputRange.cpp \ + $(srcdir)/ui/common/LineEdit.cpp \ + $(srcdir)/ui/common/Validators.cpp \ $(srcdir)/ui/explore/ArtistView.cpp \ $(srcdir)/ui/explore/ArtistsView.cpp \ $(srcdir)/ui/explore/Explore.cpp \ @@ -28,16 +39,6 @@ lms_SOURCES = \ $(srcdir)/ui/explore/ReleasesView.cpp \ $(srcdir)/ui/explore/ReleaseView.cpp \ $(srcdir)/ui/explore/TracksView.cpp \ - $(srcdir)/ui/Auth.cpp \ - $(srcdir)/ui/HomeView.cpp \ - $(srcdir)/ui/LmsApplication.cpp \ - $(srcdir)/ui/MediaPlayer.cpp \ - $(srcdir)/ui/PlayQueueView.cpp \ - $(srcdir)/ui/admin/DatabaseSettingsView.cpp \ - $(srcdir)/ui/admin/AdminWizardView.cpp \ - $(srcdir)/ui/common/InputRange.cpp \ - $(srcdir)/ui/common/LineEdit.cpp \ - $(srcdir)/ui/common/Validators.cpp \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ $(srcdir)/ui/resource/ImageResource.cpp \ $(srcdir)/ui/resource/TranscodeResource.cpp \ diff --git a/src/database/User.cpp b/src/database/User.cpp index bd1dfa2c..2919d0a2 100644 --- a/src/database/User.cpp +++ b/src/database/User.cpp @@ -31,15 +31,6 @@ User::audioBitrates = 320000, }; -const std::vector -User::audioEncodings = -{ - AudioEncoding::AUTO, - AudioEncoding::MP3, - AudioEncoding::OGA, - AudioEncoding::WEBMA, -}; - User::User() : _maxAudioBitrate(maxAudioBitrate), _isAdmin(false), diff --git a/src/database/User.hpp b/src/database/User.hpp index a422c369..582fb964 100644 --- a/src/database/User.hpp +++ b/src/database/User.hpp @@ -52,7 +52,6 @@ class User // list of audio parameters static const std::vector audioBitrates; - static const std::vector audioEncodings; User(); diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 7fff12f9..19115578 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -35,6 +35,7 @@ #include "HomeView.hpp" #include "MediaPlayer.hpp" #include "PlayQueueView.hpp" +#include "SettingsView.hpp" #include "admin/DatabaseSettingsView.hpp" #include "admin/AdminWizardView.hpp" @@ -93,6 +94,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti messageResourceBundle().use(appRoot() + "playqueue"); messageResourceBundle().use(appRoot() + "release"); messageResourceBundle().use(appRoot() + "releases"); + messageResourceBundle().use(appRoot() + "settings"); messageResourceBundle().use(appRoot() + "tracks"); messageResourceBundle().use(appRoot() + "templates"); @@ -185,6 +187,7 @@ enum IdxRoot IdxHome = 0, IdxExplore, IdxPlayQueue, + IdxSettings, IdxAdminDatabase, }; @@ -200,6 +203,7 @@ handlePathChange(Wt::WStackedWidget* stack) { "/release", IdxExplore }, { "/tracks", IdxExplore }, { "/playqueue", IdxPlayQueue }, + { "/settings", IdxSettings }, { "/admin/database", IdxAdminDatabase }, }; @@ -288,7 +292,7 @@ LmsApplication::handleAuthEvent(void) } { - auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.settings")); + auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.Settings.settings")); menuItem->setLink(Wt::WLink(Wt::WLink::InternalPath, "/settings")); menuItem->setSelectable(false); } @@ -315,6 +319,10 @@ LmsApplication::handleAuthEvent(void) auto playqueue = new PlayQueue(); mainStack->addWidget(playqueue); + auto settings = new SettingsView(); + mainStack->addWidget(settings); + + // Admin stuff auto databaseSettings = new DatabaseSettingsView(); mainStack->addWidget(databaseSettings); diff --git a/src/ui/SettingsView.cpp b/src/ui/SettingsView.cpp new file mode 100644 index 00000000..7b1ba7c4 --- /dev/null +++ b/src/ui/SettingsView.cpp @@ -0,0 +1,304 @@ +/* + * 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 "common/Validators.hpp" + +#include "utils/Logger.hpp" +#include "LmsApplication.hpp" + +#include "SettingsView.hpp" + +namespace UserInterface { + +using namespace Database; + +class SettingsModel : public Wt::WFormModel +{ + public: + // Associate each field with a unique string literal. + static const Field BitrateField; + static const Field EncodingField; + static const Field PasswordField; + static const Field PasswordConfirmField; + + SettingsModel(Wt::WObject *parent = 0) + : Wt::WFormModel(parent) + { + initializeModels(); + + addField(BitrateField); + addField(EncodingField); + addField(PasswordField); + addField(PasswordConfirmField); + + setValidator(BitrateField, createMandatoryValidator()); + setValidator(EncodingField, createMandatoryValidator()); + + loadData(); + } + + Wt::WAbstractItemModel *bitrateModel() { return _bitrateModel; } + Wt::WAbstractItemModel *encodingModel() { return _encodingModel; } + + void loadData() + { + Wt::Dbo::Transaction transaction(DboSession()); + + auto bitrate = getBitrateRow(CurrentUser()->getAudioBitrate()); + if (bitrate) + setValue(BitrateField, bitrateString(*bitrate)); + + auto encodingRow = getEncodingRow(CurrentUser()->getAudioEncoding()); + if (encodingRow) + setValue(EncodingField, encodingString(*encodingRow)); + + } + + void saveData() + { + Wt::Dbo::Transaction transaction(DboSession()); + + auto bitrateRow = getBitrateRow(Wt::asString(value(BitrateField))); + assert(bitrateRow); + CurrentUser().modify()->setAudioBitrate(bitrate(*bitrateRow)); + + auto encodingRow = getEncodingRow(Wt::asString(value(EncodingField))); + CurrentUser().modify()->setAudioEncoding(encoding(*encodingRow)); + + if (!valueText(PasswordField).empty()) + { + Database::Handler::getPasswordService().updatePassword(CurrentAuthUser(), valueText(PasswordField)); + } + } + + bool validateField(Field field) + { + Wt::WString error; + + if (field == PasswordField) + { + if (!valueText(PasswordField).empty()) + { + // Evaluate the strength of the password + auto res = Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), + CurrentAuthUser().identity(Wt::Auth::Identity::LoginName), ""); + + if (!res.isValid()) + error = res.message(); + } + else + return Wt::WFormModel::validateField(field); + } + else if (field == PasswordConfirmField) + { + if (validation(PasswordField).state() == Wt::WValidator::Valid) + { + if (valueText(PasswordField) != valueText(PasswordConfirmField)) + error = Wt::WString::tr("Lms.passwords-dont-match"); + } + } + else + { + return Wt::WFormModel::validateField(field); + } + + setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::WValidator::Valid : Wt::WValidator::Invalid, error)); + + return (validation(field).state() == Wt::WValidator::Valid); + } + + boost::optional getBitrateRow(Wt::WString value) + { + for (int i = 0; i < _bitrateModel->rowCount(); ++i) + { + if (bitrateString(i) == value) + return i; + } + + return boost::none; + } + + boost::optional getBitrateRow(std::size_t value) + { + for (int i = 0; i < _bitrateModel->rowCount(); ++i) + { + if (bitrate(i) == value) + return i; + } + + return boost::none; + } + + std::size_t bitrate(int row) + { + return boost::any_cast + (_bitrateModel->data(_encodingModel->index(row, 0), Wt::UserRole)); + } + + Wt::WString bitrateString(int row) + { + return boost::any_cast + (_bitrateModel->data(_encodingModel->index(row, 0), Wt::DisplayRole)); + } + + boost::optional getEncodingRow(Wt::WString value) + { + for (int i = 0; i < _encodingModel->rowCount(); ++i) + { + if (encodingString(i) == value) + return i; + } + + return boost::none; + } + + boost::optional getEncodingRow(Database::AudioEncoding enc) + { + for (int i = 0; i < _encodingModel->rowCount(); ++i) + { + if (encoding(i) == enc) + return i; + } + + return boost::none; + } + + Database::AudioEncoding encoding(int row) + { + return boost::any_cast + (_encodingModel->data(_encodingModel->index(row, 0), Wt::UserRole)); + } + + Wt::WString encodingString(int row) + { + return boost::any_cast + (_encodingModel->data(_encodingModel->index(row, 0), Wt::DisplayRole)); + } + + + private: + + void initializeModels() + { + + _bitrateModel = new Wt::WStringListModel(this); + + std::size_t id = 0; + for (auto bitrate : Database::User::audioBitrates) + { + _bitrateModel->addString( Wt::WString::fromUTF8(std::to_string(bitrate / 1000)) ); + _bitrateModel->setData( id++, 0, bitrate, Wt::UserRole); + } + + _encodingModel = new Wt::WStringListModel(this); + + _encodingModel->addString(Wt::WString::tr("Lms.Settings.auto")); + _encodingModel->setData(0, 0, Database::AudioEncoding::AUTO, Wt::UserRole); + + _encodingModel->addString(Wt::WString::tr("Lms.Settings.mp3")); + _encodingModel->setData(1, 0, Database::AudioEncoding::MP3, Wt::UserRole); + + _encodingModel->addString(Wt::WString::tr("Lms.Settings.oga")); + _encodingModel->setData(2, 0, Database::AudioEncoding::OGA, Wt::UserRole); + + _encodingModel->addString(Wt::WString::tr("Lms.Settings.webma")); + _encodingModel->setData(3, 0, Database::AudioEncoding::WEBMA, Wt::UserRole); + } + + Wt::WStringListModel* _bitrateModel; + Wt::WStringListModel* _encodingModel; + +}; + +const Wt::WFormModel::Field SettingsModel::BitrateField = "bitrate"; +const Wt::WFormModel::Field SettingsModel::EncodingField = "encoding"; +const Wt::WFormModel::Field SettingsModel::PasswordField = "password"; +const Wt::WFormModel::Field SettingsModel::PasswordConfirmField = "password-confirm"; + +SettingsView::SettingsView(Wt::WContainerWidget *parent) +: Wt::WTemplateFormView(parent) +{ + auto model = new SettingsModel(this); + + setTemplateText(tr("Lms.Settings.template")); + addFunction("tr", &WTemplate::Functions::tr); + addFunction("id", &WTemplate::Functions::id); + + // Password + Wt::WLineEdit *password = new Wt::WLineEdit(); + setFormWidget(SettingsModel::PasswordField, password); + password->setEchoMode(Wt::WLineEdit::Password); + + // Password confirm + Wt::WLineEdit *passwordConfirm = new Wt::WLineEdit(); + setFormWidget(SettingsModel::PasswordConfirmField, passwordConfirm); + passwordConfirm->setEchoMode(Wt::WLineEdit::Password); + + // Bitrate + Wt::WComboBox *bitrate = new Wt::WComboBox(); + setFormWidget(SettingsModel::BitrateField, bitrate); + bitrate->setModel(model->bitrateModel()); + + // Encoding + Wt::WComboBox *encoding = new Wt::WComboBox(); + setFormWidget(SettingsModel::EncodingField, encoding); + encoding->setModel(model->encodingModel()); + + // Buttons + Wt::WPushButton *saveBtn = new Wt::WPushButton(Wt::WString::tr("Lms.apply")); + bindWidget("apply-btn", saveBtn); + + Wt::WPushButton *discardBtn = new Wt::WPushButton(Wt::WString::tr("Lms.discard")); + bindWidget("discard-btn", discardBtn); + + saveBtn->clicked().connect(std::bind([=] () + { + updateModel(model); + + if (model->validate()) + { + model->saveData(); + LmsApp->notify(Wt::WString::tr("Lms.Settings.settings-saved")); + } + + // Udate the view: Delete any validation message in the view, etc. + updateView(model); + })); + + discardBtn->clicked().connect(std::bind([=] () + { + model->loadData(); + model->validate(); + updateView(model); + })); + + updateView(model); +} + +} // namespace UserInterface + + diff --git a/src/ui/SettingsView.hpp b/src/ui/SettingsView.hpp new file mode 100644 index 00000000..95708107 --- /dev/null +++ b/src/ui/SettingsView.hpp @@ -0,0 +1,34 @@ +/* + * 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 + +namespace UserInterface { + +class SettingsView : public Wt::WTemplateFormView +{ + public: + SettingsView(Wt::WContainerWidget *parent = 0); +}; + +} // namespace UserInterface + diff --git a/src/ui/admin/AdminWizardView.cpp b/src/ui/admin/AdminWizardView.cpp index 44cc6e76..b0768c3c 100644 --- a/src/ui/admin/AdminWizardView.cpp +++ b/src/ui/admin/AdminWizardView.cpp @@ -97,7 +97,7 @@ class AdminWizardModel : public Wt::WFormModel if (validation(PasswordField).state() == Wt::WValidator::Valid) { if (valueText(PasswordField) != valueText(PasswordConfirmField)) - error = Wt::WString::tr("Wt.Auth.passwords-dont-match"); + error = Wt::WString::tr("Lms.passwords-dont-match"); } } else diff --git a/src/ui/admin/DatabaseSettingsView.cpp b/src/ui/admin/DatabaseSettingsView.cpp index 6db7b934..b5b0b507 100644 --- a/src/ui/admin/DatabaseSettingsView.cpp +++ b/src/ui/admin/DatabaseSettingsView.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -177,7 +176,8 @@ class DatabaseSettingsModel : public Wt::WFormModel private: - void initializeModels() { + void initializeModels() + { _updatePeriodModel = new Wt::WStringListModel(this);