diff --git a/approot/main.xml b/approot/main.xml index 66143c74..1fb31ea5 100644 --- a/approot/main.xml +++ b/approot/main.xml @@ -58,7 +58,16 @@ diff --git a/approot/messages.xml b/approot/messages.xml index ba8507b7..967d65e4 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -354,7 +354,11 @@ Enable transcoding by default Feedback Types of artist relationships to display - Settings + User Interface + Audio + Subsonic API + Services + Change password Album Name Original Release Date (Ascending) Original Release Date (Descending) diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 7f041eea..fb2af82a 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -354,7 +354,11 @@ Activar por defecto la transcodificación Feedback Tipos de relaciones entre artistas a mostrar - Ajustes + Interfaz de usuario + Audio + API Subsonic + Servicios + Cambiar contraseña Nombre del álbum Fecha de publicación original (Ascendiente) Fecha de publicación original (Descendiente) diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 5ea3e9f4..ebeefb3d 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -354,7 +354,11 @@ Activer par défaut le transcodage Feedback Types de relations d'artiste à afficher - Paramètres + Interface utilisateur + Audio + API Subsonic + Services + Changement de mot de passe Nom de l'album Date de sortie originale (Croissant) Date de sortie originale (Décroissant) diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 10b38d3c..49018118 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -354,7 +354,11 @@ Abilita transcodifica di default Feedback Tipi di relazioni artistiche da visualizzare - Impostazioni + Interfaccia utente + Audio + API Subsonic + Servizi + Cambia password Nome dell'album Data di uscita originale (Crescente) Data di uscita originale (Decrescente) diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 8e9d3d3d..023dcdc0 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -380,7 +380,11 @@ Aktywuj domyślnie transkodowanie Feedback Typy powiązań artystów do wyświetlenia - Ustawienia + Interfejs użytkownika + Dźwięk + API Subsonic + Usługi + Zmień hasło Nazwa albumu Oryginalna data wydania (Rosnąco) Oryginalna data wydania (Malejąco) diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 801d38f3..9a7e6bb7 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -325,7 +325,11 @@ 默认启用转码 反馈 要显示的艺术家关系类型 - 设置 + 用户界面 + 音频 + Subsonic 应用程序接口 + 服务 + 更改密码 专辑名称 原始发行日期(升序) 原始发行日期(降序) diff --git a/approot/settings-audio.xml b/approot/settings-audio.xml new file mode 100644 index 00000000..097350bc --- /dev/null +++ b/approot/settings-audio.xml @@ -0,0 +1,79 @@ + + + + +
+ ${tr:Lms.Settings.audio} + + +
+ + ${transcoding-mode class="form-control"} +
+ ${transcoding-mode-info} +
+
+
+ + ${transcoding-output-format class="form-control"} +
+ ${transcoding-output-format-info} +
+
+
+ +
+ ${transcoding-output-bitrate class="form-control"} + kbps +
+
+ ${transcoding-output-bitrate-info} +
+
+
+ + ${replaygain-mode class="form-control"} +
+ ${replaygain-mode-info} +
+
+
+ +
+ ${replaygain-preamp class="form-control"} + dB +
+ ${replaygain-preamp-info} +
+
+
+
+ +
+ ${replaygain-preamp-no-rg-info class="form-control"} + dB +
+ ${replaygain-preamp-no-rg-info-info} +
+
+
+
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ +
diff --git a/approot/settings-password.xml b/approot/settings-password.xml new file mode 100644 index 00000000..add3b0f0 --- /dev/null +++ b/approot/settings-password.xml @@ -0,0 +1,41 @@ + + + + +
+ ${tr:Lms.Settings.change-password} + ${} +
+ + ${password-old class="form-control"} +
+ ${password-old-info class="help-block"} +
+
+ ${
} +
+ + ${password class="form-control"} +
+ ${password-info class="help-block"} +
+
+
+ + ${password-confirm class="form-control"} +
+ ${password-confirm-info class="help-block"} +
+
+
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ +
diff --git a/approot/settings-services.xml b/approot/settings-services.xml new file mode 100644 index 00000000..62598371 --- /dev/null +++ b/approot/settings-services.xml @@ -0,0 +1,42 @@ + + + + +
+ ${tr:Lms.Settings.services} +
+ + ${scrobbling-backend class="form-control"} +
+ ${scrobbling-backend-info class="help-block"} +
+
+
+ + ${feedback-backend class="form-control"} +
+ ${feedback-backend-info class="help-block"} +
+
+
+ +
+ ${listenbrainz-token class="form-control"} + ${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${listenbrainz-token-info class="help-block"} +
+
+
+
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ +
diff --git a/approot/settings-subsonic.xml b/approot/settings-subsonic.xml new file mode 100644 index 00000000..e94c7484 --- /dev/null +++ b/approot/settings-subsonic.xml @@ -0,0 +1,72 @@ + + + + +
+ ${tr:Lms.Settings.subsonic-api} +
+ +
+ ${subsonic-token-regen-btn class="btn btn-outline-secondary"} + ${subsonic-token-del-btn class="btn btn-outline-warning"} + ${subsonic-token class="form-control"} + ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${subsonic-token-info class="help-block"} +
+
+
+
+
+ ${subsonic-enable-transcoding-by-default class="form-check-input"} + +
+ ${subsonic-enable-transcoding-by-default-info} +
+
+
+
+ + ${subsonic-transcoding-output-format class="form-control"} +
+ ${subsonic-transcoding-output-format-info class="help-block"} +
+
+
+ +
+ ${subsonic-transcoding-output-bitrate class="form-control"} + kbps +
+
+ ${subsonic-transcoding-output-bitrate-info class="help-block"} +
+
+
+ + ${subsonic-artist-list-mode class="form-control"} +
+ ${subsonic-artist-list-mode-info class="help-block"} +
+
+
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ +
diff --git a/approot/settings-ui.xml b/approot/settings-ui.xml new file mode 100644 index 00000000..ed4aad56 --- /dev/null +++ b/approot/settings-ui.xml @@ -0,0 +1,41 @@ + + + + +
+ ${tr:Lms.Settings.user-interface} +
+ + ${artist-release-sort-method class="form-control"} +
+ ${artist-release-sort-method-info} +
+
+
+
+ ${enable-inline-artist-relationships class="form-check-input"} + +
+ ${enable-inline-artist-relationships} +
+
+
+
+ + ${inline-artist-relationships class="form-control"} +
+ ${inline-artist-relationships-info} +
+
+
+
+ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} +
+ +
diff --git a/approot/settings.xml b/approot/settings.xml deleted file mode 100644 index 46a4bafa..00000000 --- a/approot/settings.xml +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - -
- ${tr:Lms.Settings.user-interface} -
- - ${artist-release-sort-method class="form-control"} -
- ${artist-release-sort-method-info} -
-
-
-
- ${enable-inline-artist-relationships class="form-check-input"} - -
- ${enable-inline-artist-relationships} -
-
-
-
- - ${inline-artist-relationships class="form-control"} -
- ${inline-artist-relationships-info} -
-
- ${tr:Lms.Settings.audio} - - -
- - ${transcoding-mode class="form-control"} -
- ${transcoding-mode-info} -
-
-
- - ${transcoding-output-format class="form-control"} -
- ${transcoding-output-format-info} -
-
-
- -
- ${transcoding-output-bitrate class="form-control"} - kbps -
-
- ${transcoding-output-bitrate-info} -
-
-
- - ${replaygain-mode class="form-control"} -
- ${replaygain-mode-info} -
-
-
- -
- ${replaygain-preamp class="form-control"} - dB -
- ${replaygain-preamp-info} -
-
-
-
- -
- ${replaygain-preamp-no-rg-info class="form-control"} - dB -
- ${replaygain-preamp-no-rg-info-info} -
-
-
- ${} - ${tr:Lms.Settings.subsonic-api} -
- -
- ${subsonic-token-regen-btn class="btn btn-outline-secondary"} - ${subsonic-token-del-btn class="btn btn-outline-warning"} - ${subsonic-token class="form-control"} - ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} -
- ${subsonic-token-info class="help-block"} -
-
-
-
-
- ${subsonic-enable-transcoding-by-default class="form-check-input"} - -
- ${subsonic-enable-transcoding-by-default-info} -
-
-
-
- - ${subsonic-transcoding-output-format class="form-control"} -
- ${subsonic-transcoding-output-format-info class="help-block"} -
-
-
- -
- ${subsonic-transcoding-output-bitrate class="form-control"} - kbps -
-
- ${subsonic-transcoding-output-bitrate-info class="help-block"} -
-
-
- - ${subsonic-artist-list-mode class="form-control"} -
- ${subsonic-artist-list-mode-info class="help-block"} -
-
- ${
} - ${tr:Lms.Settings.services} -
- - ${scrobbling-backend class="form-control"} -
- ${scrobbling-backend-info class="help-block"} -
-
-
- - ${feedback-backend class="form-control"} -
- ${feedback-backend-info class="help-block"} -
-
-
- -
- ${listenbrainz-token class="form-control"} - ${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"} -
- ${listenbrainz-token-info class="help-block"} -
-
-
- ${} - ${tr:Lms.Settings.change-password} - ${} -
- - ${password-old class="form-control"} -
- ${password-old-info class="help-block"} -
-
- ${
} -
- - ${password class="form-control"} -
- ${password-info class="help-block"} -
-
-
- - ${password-confirm class="form-control"} -
- ${password-confirm-info class="help-block"} -
-
- ${
} -
-
- ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} -
- -
diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index a2dec023..6ac4bc93 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -10,10 +10,17 @@ add_executable(lms ui/ModalManager.cpp ui/NotificationContainer.cpp ui/PlayQueue.cpp - ui/SettingsView.cpp + ui/settings/AudioSettingsView.cpp + ui/settings/PasswordSettingsView.cpp + ui/settings/ServicesSettingsView.cpp + ui/settings/SettingsView.cpp + ui/settings/SettingsViewUtils.cpp + ui/settings/SubsonicSettingsView.cpp + ui/settings/UISettingsView.cpp ui/State.cpp ui/Tooltip.cpp ui/Utils.cpp + ui/admin/AdminView.cpp ui/admin/debug/Database.cpp ui/admin/debug/Tracing.cpp ui/admin/About.cpp @@ -27,6 +34,7 @@ add_executable(lms ui/admin/UserView.cpp ui/admin/UsersView.cpp ui/common/DoubleValidator.cpp + ui/common/PathRouter.cpp ui/common/InfiniteScrollingContainer.cpp ui/common/LoadingIndicator.cpp ui/common/LoginNameValidator.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 83b89728..126cbcfd 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -53,20 +53,15 @@ #include "ModalManager.hpp" #include "NotificationContainer.hpp" #include "PlayQueue.hpp" -#include "SettingsView.hpp" #include "admin/About.hpp" -#include "admin/DebugToolsView.hpp" +#include "admin/AdminView.hpp" #include "admin/InitWizardView.hpp" -#include "admin/MediaLibrariesView.hpp" -#include "admin/ScanSettingsView.hpp" -#include "admin/ScannerController.hpp" -#include "admin/UserView.hpp" -#include "admin/UsersView.hpp" #include "common/Template.hpp" #include "explore/Explore.hpp" #include "explore/Filters.hpp" #include "resource/ArtworkResource.hpp" #include "resource/AudioTranscodingResource.hpp" +#include "settings/SettingsView.hpp" namespace lms::ui { @@ -103,7 +98,11 @@ namespace lms::ui res->use(appRoot + "playqueue"); res->use(appRoot + "release"); res->use(appRoot + "releases"); - res->use(appRoot + "settings"); + res->use(appRoot + "settings-audio"); + res->use(appRoot + "settings-password"); + res->use(appRoot + "settings-services"); + res->use(appRoot + "settings-subsonic"); + res->use(appRoot + "settings-ui"); res->use(appRoot + "tracklist"); res->use(appRoot + "tracklists"); res->use(appRoot + "tracks"); @@ -134,12 +133,7 @@ namespace lms::ui IdxExplore = 0, IdxPlayQueue, IdxSettings, - IdxAdminLibraries, - IdxAdminScanSettings, - IdxAdminScanner, - IdxAdminUsers, - IdxAdminUser, - IdxAdminDebugTools, + IdxAdmin, }; void handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) @@ -159,13 +153,8 @@ namespace lms::ui { "/tracklists", IdxExplore, false, Wt::WString::tr("Lms.Explore.tracklists") }, { "/tracklist", IdxExplore, false, std::nullopt }, { "/playqueue", IdxPlayQueue, false, Wt::WString::tr("Lms.PlayQueue.playqueue") }, - { "/settings", IdxSettings, false, Wt::WString::tr("Lms.Settings.settings") }, - { "/admin/libraries", IdxAdminLibraries, true, Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries") }, - { "/admin/scan-settings", IdxAdminScanSettings, true, Wt::WString::tr("Lms.Admin.Database.scan-settings") }, - { "/admin/scanner", IdxAdminScanner, true, Wt::WString::tr("Lms.Admin.ScannerController.scanner") }, - { "/admin/users", IdxAdminUsers, true, Wt::WString::tr("Lms.Admin.Users.users") }, - { "/admin/user", IdxAdminUser, true, std::nullopt }, - { "/admin/debug-tools", IdxAdminDebugTools, true, Wt::WString::tr("Lms.Admin.DebugTools.debug-tools") }, + { "/settings", IdxSettings, false, std::nullopt }, + { "/admin", IdxAdmin, true, std::nullopt }, }; LMS_LOG(UI, DEBUG, "Internal path changed to '" << wApp->internalPath() << "'"); @@ -459,7 +448,21 @@ namespace lms::ui Filters* filters{ navbar->bindNew("filters") }; navbar->bindString("username", std::string{ getUserLoginName() }, Wt::TextFormat::Plain); - navbar->bindNew("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings")); + + navbar->bindNew("settings-ui", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/ui" }, Wt::WString::tr("Lms.Settings.menu-ui")); + navbar->bindNew("settings-audio", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/audio" }, Wt::WString::tr("Lms.Settings.menu-audio")); + if (core::Service::get()->getBool("api-subsonic", true)) + { + navbar->setCondition("if-has-subsonic-api-menu", true); + navbar->bindNew("settings-subsonic", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/subsonic" }, Wt::WString::tr("Lms.Settings.menu-subsonic")); + } + navbar->bindNew("settings-services", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/services" }, Wt::WString::tr("Lms.Settings.menu-services")); + + if (getAuthBackend() == AuthenticationBackend::Internal) + { + navbar->setCondition("if-has-change-password-menu", true); + navbar->bindNew("settings-password", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/password" }, Wt::WString::tr("Lms.Settings.menu-password")); + } { Wt::WAnchor* logout{ navbar->bindNew("logout") }; @@ -499,14 +502,7 @@ namespace lms::ui // Admin stuff if (getUserType() == db::UserType::ADMIN) - { - mainStack->addNew(); - mainStack->addNew(); - mainStack->addNew(); - mainStack->addNew(); - mainStack->addNew(); - mainStack->addNew(); - } + mainStack->addNew(); explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity()); diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp deleted file mode 100644 index 4be8126c..00000000 --- a/src/lms/ui/SettingsView.cpp +++ /dev/null @@ -1,789 +0,0 @@ -/* - * 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 "SettingsView.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/IConfig.hpp" -#include "core/Service.hpp" -#include "core/String.hpp" -#include "core/UUID.hpp" - -#include "database/Session.hpp" -#include "database/Types.hpp" -#include "database/objects/User.hpp" -#include "services/auth/IAuthTokenService.hpp" -#include "services/auth/IPasswordService.hpp" - -#include "LmsApplication.hpp" -#include "MediaPlayer.hpp" -#include "Tooltip.hpp" -#include "common/DoubleValidator.hpp" -#include "common/MandatoryValidator.hpp" -#include "common/PasswordValidator.hpp" -#include "common/UUIDValidator.hpp" -#include "common/ValueStringModel.hpp" - -namespace lms::ui -{ - namespace - { - // highly unefficient hack to make WSelectionBox work with Wt::WFormModel - class SelectionBox : public Wt::WSelectionBox - { - public: - static inline constexpr std::string_view valueSeparator{ ", " }; - - private: - void setValueText(const Wt::WString& values) override - { - std::set selectedIndexes; - const std::string strValues{ values.toUTF8() }; - for (std::string_view value : core::stringUtils::splitString(strValues, valueSeparator)) - { - const int index{ findText(std::string{ value }) }; - if (index >= 0) - selectedIndexes.insert(index); - } - - setSelectedIndexes(selectedIndexes); - } - - Wt::WString valueText() const override - { - Wt::WString res; - for (int index : selectedIndexes()) - { - if (!res.empty()) - res += std::string{ valueSeparator }; - res += itemText(index); - } - - return res; - } - }; - } // namespace - - class SettingsModel : public Wt::WFormModel - { - public: - // Associate each field with a unique string literal. - static inline const Field ArtistReleaseSortMethodField{ "artist-release-sort-method" }; - static inline const Field EnableInlineArtistRelationships{ "enable-inline-artist-relationships" }; - static inline const Field InlineArtistRelationships{ "inline-artist-relationships" }; - static inline const Field TranscodingModeField{ "transcoding-mode" }; - static inline const Field TranscodeFormatField{ "transcoding-output-format" }; - static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" }; - static inline const Field ReplayGainModeField{ "replaygain-mode" }; - static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; - static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; - static inline const Field SubsonicTokenField{ "subsonic-token" }; - static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" }; - static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; - static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" }; - static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcoding-output-bitrate" }; - static inline const Field FeedbackBackendField{ "feedback-backend" }; - static inline const Field ScrobblingBackendField{ "scrobbling-backend" }; - static inline const Field ListenBrainzTokenField{ "listenbrainz-token" }; - static inline const Field PasswordOldField{ "password-old" }; - static inline const Field PasswordField{ "password" }; - static inline const Field PasswordConfirmField{ "password-confirm" }; - - using ArtistReleaseSortMethodModel = ValueStringModel; - using ArtistRelationshipsModel = ValueStringModel; - using TranscodingModeModel = ValueStringModel; - using ReplayGainModeModel = ValueStringModel; - using FeedbackBackendModel = ValueStringModel; - using ScrobblingBackendModel = ValueStringModel; - - SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword, auth::IAuthTokenService& authTokenService) - : _authPasswordService{ authPasswordService } - , _withOldPassword{ withOldPassword } - , _authTokenService{ authTokenService } - { - initializeModels(); - - addField(ArtistReleaseSortMethodField); - addField(EnableInlineArtistRelationships); - addField(InlineArtistRelationships); - addField(TranscodingModeField); - addField(TranscodeBitrateField); - addField(TranscodeFormatField); - addField(ReplayGainModeField); - addField(ReplayGainPreAmpGainField); - addField(ReplayGainPreAmpGainIfNoInfoField); - addField(SubsonicTokenField); - addField(SubsonicEnableTranscodingByDefault); - addField(SubsonicTranscodingOutputBitrateField); - addField(SubsonicTranscodingOutputFormatField); - addField(FeedbackBackendField); - addField(ScrobblingBackendField); - addField(ListenBrainzTokenField); - - setValidator(SubsonicTokenField, createUUIDValidator()); - setValidator(ListenBrainzTokenField, createMandatoryValidator()); - - if (_authPasswordService) - { - if (_withOldPassword) - { - addField(PasswordOldField); - setValidator(PasswordOldField, createPasswordCheckValidator(*_authPasswordService)); - } - - addField(PasswordField); - setValidator(PasswordField, createPasswordStrengthValidator(*authPasswordService, [] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; })); - addField(PasswordConfirmField); - } - - setValidator(ArtistReleaseSortMethodField, createMandatoryValidator()); - setValidator(TranscodingModeField, createMandatoryValidator()); - setValidator(TranscodeBitrateField, createMandatoryValidator()); - setValidator(TranscodeFormatField, createMandatoryValidator()); - setValidator(ReplayGainModeField, createMandatoryValidator()); - auto createPreAmpValidator{ [] { - return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - } }; - - setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); - setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); - setValidator(SubsonicTranscodingOutputBitrateField, createMandatoryValidator()); - setValidator(SubsonicTranscodingOutputFormatField, createMandatoryValidator()); - - loadData(); - } - - std::shared_ptr getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; } - std::shared_ptr getArtistRelationshipsModel() { return _artistRelationshipsModel; } - std::shared_ptr getTranscodingModeModel() { return _transcodingModeModeModel; } - std::shared_ptr getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; } - std::shared_ptr getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; } - std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } - std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } - std::shared_ptr getFeedbackBackendModel() { return _feedbackBackendModel; } - std::shared_ptr getScrobblingBackendModel() { return _scrobblingBackendModel; } - - void saveData() - { - auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; - - db::User::pointer user{ LmsApp->getUser() }; - - { - const auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) }; - if (artistReleaseSortMethodRow) - user.modify()->setUIArtistReleaseSortMethod(_artistReleaseSortMethodModel->getValue(*artistReleaseSortMethodRow)); - - const bool enableInlineArtistRelationships{ Wt::asNumber(value(EnableInlineArtistRelationships)) != 0 }; - user.modify()->setUIEnableInlineArtistRelationships(enableInlineArtistRelationships); - - core::EnumSet artistLinkTypes; - const std::string relationships{ valueText(InlineArtistRelationships).toUTF8() }; - for (std::string_view relationship : core::stringUtils::splitString(relationships, SelectionBox::valueSeparator)) - { - auto artistRelationshipRow{ _artistRelationshipsModel->getRowFromString(Wt::WString{ std::string{ relationship } }) }; - if (artistRelationshipRow) - artistLinkTypes.insert(_artistRelationshipsModel->getValue(*artistRelationshipRow)); - } - - user.modify()->setUIInlineArtistRelationships(artistLinkTypes); - } - - { - MediaPlayer::Settings settings; - - auto transcodingModeRow{ _transcodingModeModeModel->getRowFromString(valueText(TranscodingModeField)) }; - if (transcodingModeRow) - settings.transcoding.mode = _transcodingModeModeModel->getValue(*transcodingModeRow); - - auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; - if (transcodingOutputFormatRow) - settings.transcoding.format = _transcodingOutputFormatModel->getValue(*transcodingOutputFormatRow); - - auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; - if (transcodingOutputBitrateRow) - settings.transcoding.bitrate = _transcodingOutputBitrateModel->getValue(*transcodingOutputBitrateRow); - - auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) }; - if (replayGainModeRow) - settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); - - settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); - settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField)); - - LmsApp->getMediaPlayer().setSettings(settings); - } - - // Subsonic API - { - const std::string token{ Wt::asString(value(SubsonicTokenField)).toUTF8() }; - - if (token.empty()) - { - _authTokenService.clearAuthTokens("subsonic", user->getId()); - } - else - { - // Consider there must be only one token - bool hasNonMatchingToken{ false }; - bool hasMatchingToken{ false }; - _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { - if (storedToken == token) - hasMatchingToken = true; - else - hasNonMatchingToken = true; - }); - - if (!hasMatchingToken || hasNonMatchingToken) - { - _authTokenService.clearAuthTokens("subsonic", user->getId()); - _authTokenService.createAuthToken("subsonic", user->getId(), token); - } - } - - bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 }; - user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault); - - auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) }; - if (subsonicTranscodingOutputBitrateRow) - user.modify()->setSubsonicDefaultTranscodingOutputBitrate(_transcodingOutputBitrateModel->getValue(*subsonicTranscodingOutputBitrateRow)); - - auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(SubsonicTranscodingOutputFormatField)) }; - if (subsonicTranscodingOutputFormatRow) - user.modify()->setSubsonicDefaultTranscodintOutputFormat(_transcodingOutputFormatModel->getValue(*subsonicTranscodingOutputFormatRow)); - - auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) }; - if (subsonicArtistListModeRow) - user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); - } - - { - if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) }) - user.modify()->setFeedbackBackend(_feedbackBackendModel->getValue(*feedbackBackendRow)); - - user.modify()->setListenBrainzToken(Wt::asString(value(ListenBrainzTokenField)).toUTF8()); - } - - { - if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromString(valueText(ScrobblingBackendField)) }) - user.modify()->setScrobblingBackend(_scrobblingBackendModel->getValue(*scrobblingBackendRow)); - - user.modify()->setListenBrainzToken(Wt::asString(value(ListenBrainzTokenField)).toUTF8()); - } - - if (_authPasswordService && !valueText(PasswordField).empty()) - { - _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); - _authTokenService.clearAuthTokens("ui", user->getId()); - } - } - - void loadData() - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const db::User::pointer user{ LmsApp->getUser() }; - - // UI - { - auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) }; - if (artistReleaseSortMethodRow) - setValue(ArtistReleaseSortMethodField, _artistReleaseSortMethodModel->getString(*artistReleaseSortMethodRow)); - - setValue(EnableInlineArtistRelationships, user->getUIEnableInlineArtistRelationships()); - setReadOnly(InlineArtistRelationships, !user->getUIEnableInlineArtistRelationships()); - - Wt::WString inlineArtistRelationships; - for (db::TrackArtistLinkType artistLinkType : user->getUIInlineArtistRelationships()) - { - if (auto artistRelationshipsRow{ _artistRelationshipsModel->getRowFromValue(artistLinkType) }) - { - if (!inlineArtistRelationships.empty()) - inlineArtistRelationships += std::string{ SelectionBox::valueSeparator }; - inlineArtistRelationships += _artistRelationshipsModel->getString(*artistRelationshipsRow); - } - } - - setValue(InlineArtistRelationships, inlineArtistRelationships); - } - - // Audio - { - const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; - - auto transcodingModeRow{ _transcodingModeModeModel->getRowFromValue(settings.transcoding.mode) }; - if (transcodingModeRow) - setValue(TranscodingModeField, _transcodingModeModeModel->getString(*transcodingModeRow)); - - auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(settings.transcoding.format) }; - if (transcodingOutputFormatRow) - setValue(TranscodeFormatField, _transcodingOutputFormatModel->getString(*transcodingOutputFormatRow)); - - auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(settings.transcoding.bitrate) }; - if (transcodingOutputBitrateRow) - setValue(TranscodeBitrateField, _transcodingOutputBitrateModel->getString(*transcodingOutputBitrateRow)); - - { - const bool useTranscoding{ settings.transcoding.mode != MediaPlayer::Settings::Transcoding::Mode::Never }; - setReadOnly(SettingsModel::TranscodeFormatField, !useTranscoding); - setReadOnly(SettingsModel::TranscodeBitrateField, !useTranscoding); - } - - auto replayGainModeRow{ _replayGainModeModel->getRowFromValue(settings.replayGain.mode) }; - if (replayGainModeRow) - setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); - - setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); - setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); - } - - // Subsonic - { - // Consider there is only one auth token - _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { - if (Wt::asString(value(SubsonicTokenField)).empty()) - setValue(SubsonicTokenField, Wt::WString::fromUTF8(std::string{ storedToken })); - }); - - setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault()); - - auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) }; - if (subsonicTranscodingOutputBitrateRow) - setValue(SubsonicTranscodingOutputBitrateField, _transcodingOutputBitrateModel->getString(*subsonicTranscodingOutputBitrateRow)); - - auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputFormat()) }; - if (subsonicTranscodingOutputFormatRow) - setValue(SubsonicTranscodingOutputFormatField, _transcodingOutputFormatModel->getString(*subsonicTranscodingOutputFormatRow)); - - auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) }; - if (subsonicArtistListModeRow) - setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow)); - } - - { - if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromValue(user->getFeedbackBackend()) }) - setValue(FeedbackBackendField, _feedbackBackendModel->getString(*feedbackBackendRow)); - - if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) }) - setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow)); - - if (const auto listenBrainzToken{ user->getListenBrainzToken() }; !listenBrainzToken.empty()) - setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken })); - - { - const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz }; - setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); - validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); - } - } - if (_authPasswordService) - { - if (_withOldPassword) - setValue(PasswordOldField, ""); - - setValue(PasswordField, ""); - setValue(PasswordConfirmField, ""); - } - } - - private: - bool validateField(Field field) - { - Wt::WString error; - - if (field == PasswordOldField) - { - if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty()) - error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); - else - return Wt::WFormModel::validateField(field); - } - else if (field == PasswordField) - { - if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty()) - error = Wt::WString::tr("Wt.WValidator.Invalid"); - else - return Wt::WFormModel::validateField(field); - } - else if (field == PasswordConfirmField) - { - if (validation(PasswordField).state() == Wt::ValidationState::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::ValidationState::Valid : Wt::ValidationState::Invalid, error)); - - return (validation(field).state() == Wt::ValidationState::Valid); - } - - private: - void initializeModels() - { - _artistReleaseSortMethodModel = std::make_shared(); - _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-asc"), db::ReleaseSortMethod::DateAsc); - _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-desc"), db::ReleaseSortMethod::DateDesc); - _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-asc"), db::ReleaseSortMethod::OriginalDate); - _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-desc"), db::ReleaseSortMethod::OriginalDateDesc); - _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name); - - _artistRelationshipsModel = std::make_shared(); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.composer", 2), db::TrackArtistLinkType::Composer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.conductor", 2), db::TrackArtistLinkType::Conductor); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.lyricist", 2), db::TrackArtistLinkType::Lyricist); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.mixer", 2), db::TrackArtistLinkType::Mixer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.performer", 2), db::TrackArtistLinkType::Performer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.producer", 2), db::TrackArtistLinkType::Producer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.remixer", 2), db::TrackArtistLinkType::Remixer); - - _transcodingModeModeModel = std::make_shared(); - _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); - _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never); - _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported); - - _transcodingOutputBitrateModel = std::make_shared>(); - db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) { - _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); - }); - - _transcodingOutputFormatModel = std::make_shared>(); - _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3); - _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS); - _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS); - - _replayGainModeModel = std::make_shared(); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); - - _subsonicArtistListModeModel = std::make_shared>(); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), db::SubsonicArtistListMode::AllArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), db::SubsonicArtistListMode::ReleaseArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), db::SubsonicArtistListMode::TrackArtists); - - _feedbackBackendModel = std::make_shared>(); - _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::FeedbackBackend::Internal); - _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::FeedbackBackend::ListenBrainz); - - _scrobblingBackendModel = std::make_shared>(); - _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::ScrobblingBackend::Internal); - _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::ScrobblingBackend::ListenBrainz); - } - - auth::IPasswordService* _authPasswordService{}; - bool _withOldPassword{}; - - auth::IAuthTokenService& _authTokenService; - - std::shared_ptr _artistReleaseSortMethodModel; - std::shared_ptr _artistRelationshipsModel; - std::shared_ptr _transcodingModeModeModel; - std::shared_ptr> _transcodingOutputBitrateModel; - std::shared_ptr> _transcodingOutputFormatModel; - std::shared_ptr _replayGainModeModel; - std::shared_ptr> _subsonicArtistListModeModel; - std::shared_ptr _feedbackBackendModel; - std::shared_ptr _scrobblingBackendModel; - }; - - SettingsView::SettingsView() - { - wApp->internalPathChanged().connect(this, [this] { - refreshView(); - }); - - LmsApp->getMediaPlayer().settingsLoaded.connect([this] { - refreshView(); - }); - - refreshView(); - } - - void SettingsView::refreshView() - { - if (!wApp->internalPathMatches("/settings")) - return; - - clear(); - - // Hack to wait for the audio player know the settings applied - if (!LmsApp->getMediaPlayer().getSettings()) - return; - - auto t{ addNew(Wt::WString::tr("Lms.Settings.template")) }; - - auth::IPasswordService* authPasswordService{}; - if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal) - { - authPasswordService = core::Service::get(); - assert(authPasswordService->canSetPasswords()); - } - - auto model{ std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong(), *core::Service::get()) }; - if (authPasswordService) - { - t->setCondition("if-has-change-password", true); - - // Old password - if (!LmsApp->isUserAuthStrong()) - { - t->setCondition("if-has-old-password", true); - - auto oldPassword{ std::make_unique() }; - oldPassword->setEchoMode(Wt::EchoMode::Password); - oldPassword->setAttributeValue("autocomplete", "current-password"); - t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); - } - - // Password - auto password{ std::make_unique() }; - password->setEchoMode(Wt::EchoMode::Password); - password->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordField, std::move(password)); - - // Password confirm - auto passwordConfirm{ std::make_unique() }; - passwordConfirm->setEchoMode(Wt::EchoMode::Password); - passwordConfirm->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); - } - - // User interface - { - auto artistReleaseSortMethod{ std::make_unique() }; - artistReleaseSortMethod->setModel(model->getArtistReleaseSortMethodModel()); - t->setFormWidget(SettingsModel::ArtistReleaseSortMethodField, std::move(artistReleaseSortMethod)); - - auto enableInlineArtistRelationships{ std::make_unique() }; - auto inlineArtistRelationships{ std::make_unique() }; - inlineArtistRelationships->setSelectionMode(Wt::SelectionMode::Extended); - inlineArtistRelationships->setVerticalSize(3); - inlineArtistRelationships->setModel(model->getArtistRelationshipsModel()); - - auto updateInlineArtistRelationships{ [=](bool readOnly) { - model->setReadOnly(SettingsModel::InlineArtistRelationships, readOnly); - t->updateModel(model.get()); - t->updateView(model.get()); - } }; - enableInlineArtistRelationships->checked().connect([=] { updateInlineArtistRelationships(false); }); - enableInlineArtistRelationships->unChecked().connect([=] { updateInlineArtistRelationships(true); }); - - t->setFormWidget(SettingsModel::EnableInlineArtistRelationships, std::move(enableInlineArtistRelationships)); - t->setFormWidget(SettingsModel::InlineArtistRelationships, std::move(inlineArtistRelationships)); - } - - // Audio - { - // Transcode - auto transcodingMode{ std::make_unique() }; - transcodingMode->setModel(model->getTranscodingModeModel()); - transcodingMode->activated().connect([=](int row) { - const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcoding::Mode::Never }; - model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); - model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - t->setFormWidget(SettingsModel::TranscodingModeField, std::move(transcodingMode)); - - // Format - auto transcodingOutputFormat{ std::make_unique() }; - transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); - t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodingOutputFormat)); - - // Bitrate - auto transcodingOutputBitrate{ std::make_unique() }; - transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); - t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate)); - - // Replay gain mode - auto replayGainMode{ std::make_unique() }; - replayGainMode->activated().connect([=](int row) { - const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None }; - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable); - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - replayGainMode->setModel(model->getReplayGainModeModel()); - t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode)); - - // Replay gain preampGain - auto replayGainPreampGain{ std::make_unique() }; - replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); - - // Replay gain preampGain if no info - auto replayGainPreampGainIfNoInfo{ std::make_unique() }; - replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo)); - - if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None) - { - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true); - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, true); - } - } - - // Subsonic - { - t->setCondition("if-has-subsonic-api", core::Service::get()->getBool("api-subsonic", true)); - t->setCondition("if-has-subsonic-token-usage", core::Service::get()->getBool("api-subsonic-support-user-password-auth", true)); - - auto subsonicToken{ std::make_unique() }; - Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() }; - subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password); - subsonicTokenPtr->setReadOnly(true); - t->setFormWidget(SettingsModel::SubsonicTokenField, std::move(subsonicToken)); - - auto subsonicTokenRegenBtn{ std::make_unique(Wt::WString::tr("Lms.Settings.regen-token")) }; - subsonicTokenRegenBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(std::string{ core::UUID::generate().getAsString() })); - }); - t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn)); - - auto subsonicTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; - subsonicTokenVisibilityBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); - }); - t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn)); - - auto subsonicTokenDelBtn{ std::make_unique(Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) }; - subsonicTokenDelBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setValueText(""); - }); - t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn)); - - // Enable transcoding by default - t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique()); - - // Format - auto transcodingOutputFormat{ std::make_unique() }; - transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodingOutputFormat)); - - // Bitrate - auto transcodingOutputBitrate{ std::make_unique() }; - transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodingOutputBitrate)); - - // Artist list mode - auto artistListMode{ std::make_unique() }; - artistListMode->setModel(model->getSubsonicArtistListModeModel()); - t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); - } - - // Feedback - Wt::WComboBox* feedbackBackendRaw{}; - { - auto feedbackBackend{ std::make_unique() }; - feedbackBackend->setModel(model->getFeedbackBackendModel()); - feedbackBackendRaw = feedbackBackend.get(); - t->setFormWidget(SettingsModel::FeedbackBackendField, std::move(feedbackBackend)); - } - - // Scrobbling - Wt::WComboBox* scrobblingBackendRaw; - { - auto scrobblingBackend{ std::make_unique() }; - scrobblingBackend->setModel(model->getScrobblingBackendModel()); - scrobblingBackendRaw = scrobblingBackend.get(); - t->setFormWidget(SettingsModel::ScrobblingBackendField, std::move(scrobblingBackend)); - } - - // Backend settings - { - auto listenbrainzToken{ std::make_unique() }; - Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() }; - listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password); - t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); - - auto listenbrainzTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; - listenbrainzTokenVisibilityBtn->clicked().connect(this, [listenbrainzTokenPtr] { - listenbrainzTokenPtr->setEchoMode(listenbrainzTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); - }); - t->bindWidget("listenbrainz-token-visibility-btn", std::move(listenbrainzTokenVisibilityBtn)); - } - - auto updateListenBrainzTokenField{ [=] { - const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == db::FeedbackBackend::ListenBrainz - || model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == db::ScrobblingBackend::ListenBrainz }; - - model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable); - model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable); - t->updateModel(model.get()); - t->updateView(model.get()); - } }; - - feedbackBackendRaw->activated().connect([=] { updateListenBrainzTokenField(); }); - scrobblingBackendRaw->activated().connect([=] { updateListenBrainzTokenField(); }); - - // Buttons - Wt::WPushButton* saveBtn{ t->bindWidget("save-btn", std::make_unique(Wt::WString::tr("Lms.save"))) }; - Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))) }; - - saveBtn->clicked().connect([=] { - { - if (LmsApp->getUserType() == db::UserType::DEMO) - { - LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save")); - return; - } - } - - t->updateModel(model.get()); - - if (model->validate()) - { - model->saveData(); - LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.settings-saved")); - } - - // Udate the view: Delete any validation message in the view, etc. - t->updateView(model.get()); - }); - - discardBtn->clicked().connect([=] { - model->loadData(); - model->validate(); - t->updateView(model.get()); - }); - - t->updateView(model.get()); - - initTooltipsForWidgetTree(*t); - } - -} // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/admin/AdminView.cpp b/src/lms/ui/admin/AdminView.cpp new file mode 100644 index 00000000..058ae2ef --- /dev/null +++ b/src/lms/ui/admin/AdminView.cpp @@ -0,0 +1,45 @@ +/* + * 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 "AdminView.hpp" + +#include "DebugToolsView.hpp" +#include "MediaLibrariesView.hpp" +#include "ScanSettingsView.hpp" +#include "ScannerController.hpp" +#include "UserView.hpp" +#include "UsersView.hpp" +#include "common/PathRouter.hpp" + +namespace lms::ui +{ + AdminView::AdminView() + { + auto* router{ addNew() }; + + router->add("/admin/libraries", Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries")); + router->add("/admin/scan-settings", Wt::WString::tr("Lms.Admin.Database.scan-settings")); + router->add("/admin/scanner", Wt::WString::tr("Lms.Admin.ScannerController.scanner")); + router->add("/admin/users", Wt::WString::tr("Lms.Admin.Users.users")); + router->add("/admin/user", std::nullopt); + router->add("/admin/debug-tools", Wt::WString::tr("Lms.Admin.DebugTools.debug-tools")); + + router->activate(); + } +} // namespace lms::ui diff --git a/src/lms/ui/admin/AdminView.hpp b/src/lms/ui/admin/AdminView.hpp new file mode 100644 index 00000000..aca40cc8 --- /dev/null +++ b/src/lms/ui/admin/AdminView.hpp @@ -0,0 +1,31 @@ +/* + * 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 + +namespace lms::ui +{ + class AdminView : public Wt::WContainerWidget + { + public: + AdminView(); + }; +} // namespace lms::ui diff --git a/src/lms/ui/common/PathRouter.cpp b/src/lms/ui/common/PathRouter.cpp new file mode 100644 index 00000000..c2bf33b7 --- /dev/null +++ b/src/lms/ui/common/PathRouter.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 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 "PathRouter.hpp" + +#include + +#include "LmsApplication.hpp" + +namespace lms::ui +{ + PathRouter::PathRouter() + : _stack{ addNew() } + { + _stack->setOverflow(Wt::Overflow::Visible); + } + + void PathRouter::activate() + { + handlePathChange(); + + LmsApp->internalPathChanged().connect(this, [this] { + handlePathChange(); + }); + } + + void PathRouter::handlePathChange() + { + for (const auto& route : _routes) + { + if (LmsApp->internalPathMatches(route.path)) + { + _stack->setCurrentWidget(route.widget); + if (route.title) + LmsApp->setTitle(*route.title); + LmsApp->doJavaScript(LmsApp->javaScriptClass() + ".updateActiveNav('" + route.path + "')"); + return; + } + } + } +} // namespace lms::ui diff --git a/src/lms/ui/common/PathRouter.hpp b/src/lms/ui/common/PathRouter.hpp new file mode 100644 index 00000000..53e887d6 --- /dev/null +++ b/src/lms/ui/common/PathRouter.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2026 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 + +#include +#include +#include + +namespace lms::ui +{ + class PathRouter : public Wt::WContainerWidget + { + public: + PathRouter(); + + template + T* add(std::string_view path, std::optional title, Args&&... args) + { + T* widget{ _stack->addNew(std::forward(args)...) }; + _routes.emplace_back(Route{ std::string{ path }, widget, std::move(title) }); + return widget; + } + + // Call once after all routes are registered — performs initial routing and starts listening to path changes. + void activate(); + + private: + void handlePathChange(); + + Wt::WStackedWidget* _stack{}; + + struct Route + { + std::string path; + Wt::WWidget* widget{}; + std::optional title; + }; + std::vector _routes; + }; +} // namespace lms::ui diff --git a/src/lms/ui/settings/AudioSettingsView.cpp b/src/lms/ui/settings/AudioSettingsView.cpp new file mode 100644 index 00000000..3c67eddc --- /dev/null +++ b/src/lms/ui/settings/AudioSettingsView.cpp @@ -0,0 +1,243 @@ +/* + * 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 "AudioSettingsView.hpp" + +#include +#include +#include +#include +#include + +#include "database/Types.hpp" + +#include "LmsApplication.hpp" +#include "MediaPlayer.hpp" +#include "Tooltip.hpp" +#include "common/DoubleValidator.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/ValueStringModel.hpp" + +#include "SettingsViewUtils.hpp" + +namespace lms::ui +{ + namespace + { + class AudioSettingsModel : public Wt::WFormModel + { + public: + static inline const Field TranscodingModeField{ "transcoding-mode" }; + static inline const Field TranscodeFormatField{ "transcoding-output-format" }; + static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" }; + static inline const Field ReplayGainModeField{ "replaygain-mode" }; + static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; + static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; + + using TranscodingModeModel = ValueStringModel; + using ReplayGainModeModel = ValueStringModel; + + AudioSettingsModel() + { + _transcodingModeModeModel = std::make_shared(); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never); + _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported); + + _transcodingOutputBitrateModel = std::make_shared>(); + db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) { + _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); + }); + + _transcodingOutputFormatModel = std::make_shared>(); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS); + + _replayGainModeModel = std::make_shared(); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); + + addField(TranscodingModeField); + addField(TranscodeBitrateField); + addField(TranscodeFormatField); + addField(ReplayGainModeField); + addField(ReplayGainPreAmpGainField); + addField(ReplayGainPreAmpGainIfNoInfoField); + + setValidator(TranscodingModeField, createMandatoryValidator()); + setValidator(TranscodeBitrateField, createMandatoryValidator()); + setValidator(TranscodeFormatField, createMandatoryValidator()); + setValidator(ReplayGainModeField, createMandatoryValidator()); + + auto createPreAmpValidator{ [] { + return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + } }; + setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); + setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); + + loadData(); + } + + std::shared_ptr getTranscodingModeModel() { return _transcodingModeModeModel; } + std::shared_ptr getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; } + std::shared_ptr getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; } + std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } + + void saveData() + { + MediaPlayer::Settings settings; + + auto transcodingModeRow{ _transcodingModeModeModel->getRowFromString(valueText(TranscodingModeField)) }; + if (transcodingModeRow) + settings.transcoding.mode = _transcodingModeModeModel->getValue(*transcodingModeRow); + + auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; + if (transcodingOutputFormatRow) + settings.transcoding.format = _transcodingOutputFormatModel->getValue(*transcodingOutputFormatRow); + + auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; + if (transcodingOutputBitrateRow) + settings.transcoding.bitrate = _transcodingOutputBitrateModel->getValue(*transcodingOutputBitrateRow); + + auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) }; + if (replayGainModeRow) + settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); + + settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); + settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField)); + + LmsApp->getMediaPlayer().setSettings(settings); + } + + void loadData() + { + const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; + + auto transcodingModeRow{ _transcodingModeModeModel->getRowFromValue(settings.transcoding.mode) }; + if (transcodingModeRow) + setValue(TranscodingModeField, _transcodingModeModeModel->getString(*transcodingModeRow)); + + auto transcodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(settings.transcoding.format) }; + if (transcodingOutputFormatRow) + setValue(TranscodeFormatField, _transcodingOutputFormatModel->getString(*transcodingOutputFormatRow)); + + auto transcodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(settings.transcoding.bitrate) }; + if (transcodingOutputBitrateRow) + setValue(TranscodeBitrateField, _transcodingOutputBitrateModel->getString(*transcodingOutputBitrateRow)); + + { + const bool useTranscoding{ settings.transcoding.mode != MediaPlayer::Settings::Transcoding::Mode::Never }; + setReadOnly(AudioSettingsModel::TranscodeFormatField, !useTranscoding); + setReadOnly(AudioSettingsModel::TranscodeBitrateField, !useTranscoding); + } + + auto replayGainModeRow{ _replayGainModeModel->getRowFromValue(settings.replayGain.mode) }; + if (replayGainModeRow) + setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); + + setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); + setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); + + if (settings.replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None) + { + setReadOnly(AudioSettingsModel::ReplayGainPreAmpGainField, true); + setReadOnly(AudioSettingsModel::ReplayGainPreAmpGainIfNoInfoField, true); + } + } + + private: + std::shared_ptr _transcodingModeModeModel; + std::shared_ptr> _transcodingOutputBitrateModel; + std::shared_ptr> _transcodingOutputFormatModel; + std::shared_ptr _replayGainModeModel; + }; + } // namespace + + AudioSettingsView::AudioSettingsView() + { + wApp->internalPathChanged().connect(this, [this] { + refreshView(); + }); + + LmsApp->getMediaPlayer().settingsLoaded.connect([this] { + refreshView(); + }); + + refreshView(); + } + + void AudioSettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings/audio")) + return; + + clear(); + + if (!LmsApp->getMediaPlayer().getSettings()) + return; + + auto* t{ addNew(Wt::WString::tr("Lms.Settings.audio.template")) }; + auto model{ std::make_shared() }; + + auto transcodingMode{ std::make_unique() }; + transcodingMode->setModel(model->getTranscodingModeModel()); + transcodingMode->activated().connect([=](int row) { + const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcoding::Mode::Never }; + model->setReadOnly(AudioSettingsModel::TranscodeFormatField, !enable); + model->setReadOnly(AudioSettingsModel::TranscodeBitrateField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + t->setFormWidget(AudioSettingsModel::TranscodingModeField, std::move(transcodingMode)); + + auto transcodingOutputFormat{ std::make_unique() }; + transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); + t->setFormWidget(AudioSettingsModel::TranscodeFormatField, std::move(transcodingOutputFormat)); + + auto transcodingOutputBitrate{ std::make_unique() }; + transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); + t->setFormWidget(AudioSettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate)); + + auto replayGainMode{ std::make_unique() }; + replayGainMode->activated().connect([=](int row) { + const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None }; + model->setReadOnly(AudioSettingsModel::ReplayGainPreAmpGainField, !enable); + model->setReadOnly(AudioSettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + replayGainMode->setModel(model->getReplayGainModeModel()); + t->setFormWidget(AudioSettingsModel::ReplayGainModeField, std::move(replayGainMode)); + + auto replayGainPreampGain{ std::make_unique() }; + replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(AudioSettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); + + auto replayGainPreampGainIfNoInfo{ std::make_unique() }; + replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(AudioSettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo)); + + utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); }); + t->updateView(model.get()); + initTooltipsForWidgetTree(*t); + } +} // namespace lms::ui diff --git a/src/lms/ui/settings/AudioSettingsView.hpp b/src/lms/ui/settings/AudioSettingsView.hpp new file mode 100644 index 00000000..fa72dbe0 --- /dev/null +++ b/src/lms/ui/settings/AudioSettingsView.hpp @@ -0,0 +1,35 @@ +/* + * 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 + +namespace lms::ui +{ + class AudioSettingsView : public Wt::WContainerWidget + { + public: + AudioSettingsView(); + + private: + void refreshView(); + }; + +} // namespace lms::ui diff --git a/src/lms/ui/settings/PasswordSettingsView.cpp b/src/lms/ui/settings/PasswordSettingsView.cpp new file mode 100644 index 00000000..2c0ee5c0 --- /dev/null +++ b/src/lms/ui/settings/PasswordSettingsView.cpp @@ -0,0 +1,179 @@ +/* + * 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 "PasswordSettingsView.hpp" + +#include +#include +#include +#include + +#include "core/Service.hpp" + +#include "database/Session.hpp" +#include "database/objects/User.hpp" +#include "services/auth/IAuthTokenService.hpp" +#include "services/auth/IPasswordService.hpp" + +#include "LmsApplication.hpp" +#include "SettingsViewUtils.hpp" +#include "common/PasswordValidator.hpp" + +namespace lms::ui +{ + namespace + { + class PasswordSettingsModel : public Wt::WFormModel + { + public: + static inline const Field PasswordOldField{ "password-old" }; + static inline const Field PasswordField{ "password" }; + static inline const Field PasswordConfirmField{ "password-confirm" }; + + PasswordSettingsModel(auth::IPasswordService& authPasswordService, bool withOldPassword, auth::IAuthTokenService& authTokenService) + : _authPasswordService{ authPasswordService } + , _withOldPassword{ withOldPassword } + , _authTokenService{ authTokenService } + { + if (_withOldPassword) + { + addField(PasswordOldField); + setValidator(PasswordOldField, createPasswordCheckValidator(_authPasswordService)); + } + + addField(PasswordField); + setValidator(PasswordField, createPasswordStrengthValidator(_authPasswordService, [] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; })); + addField(PasswordConfirmField); + + loadData(); + } + + void saveData() + { + if (!valueText(PasswordField).empty()) + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + const db::User::pointer user{ LmsApp->getUser() }; + _authPasswordService.setPassword(user->getId(), valueText(PasswordField).toUTF8()); + _authTokenService.clearAuthTokens("ui", user->getId()); + } + } + + void loadData() + { + if (_withOldPassword) + setValue(PasswordOldField, ""); + setValue(PasswordField, ""); + setValue(PasswordConfirmField, ""); + } + + private: + bool validateField(Field field) + { + Wt::WString error; + + if (field == PasswordOldField) + { + if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty()) + error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); + else + return Wt::WFormModel::validateField(field); + } + else if (field == PasswordField) + { + if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty()) + error = Wt::WString::tr("Wt.WValidator.Invalid"); + else + return Wt::WFormModel::validateField(field); + } + else if (field == PasswordConfirmField) + { + if (validation(PasswordField).state() == Wt::ValidationState::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::ValidationState::Valid : Wt::ValidationState::Invalid, error)); + return (validation(field).state() == Wt::ValidationState::Valid); + } + + auth::IPasswordService& _authPasswordService; + bool _withOldPassword{}; + auth::IAuthTokenService& _authTokenService; + }; + } // namespace + + PasswordSettingsView::PasswordSettingsView() + { + wApp->internalPathChanged().connect(this, [this] { + refreshView(); + }); + + refreshView(); + } + + void PasswordSettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings/password")) + return; + + clear(); + + if (LmsApp->getAuthBackend() != AuthenticationBackend::Internal) + return; + + auth::IPasswordService* authPasswordService{ core::Service::get() }; + if (!authPasswordService) + return; + + const bool withOldPassword{ !LmsApp->isUserAuthStrong() }; + + auto* t{ addNew(Wt::WString::tr("Lms.Settings.password.template")) }; + + if (withOldPassword) + { + t->setCondition("if-has-old-password", true); + auto oldPassword{ std::make_unique() }; + oldPassword->setEchoMode(Wt::EchoMode::Password); + oldPassword->setAttributeValue("autocomplete", "current-password"); + t->setFormWidget(PasswordSettingsModel::PasswordOldField, std::move(oldPassword)); + } + + auto password{ std::make_unique() }; + password->setEchoMode(Wt::EchoMode::Password); + password->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(PasswordSettingsModel::PasswordField, std::move(password)); + + auto passwordConfirm{ std::make_unique() }; + passwordConfirm->setEchoMode(Wt::EchoMode::Password); + passwordConfirm->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(PasswordSettingsModel::PasswordConfirmField, std::move(passwordConfirm)); + + auto model{ std::make_shared(*authPasswordService, withOldPassword, *core::Service::get()) }; + + utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); }); + t->updateView(model.get()); + } +} // namespace lms::ui diff --git a/src/lms/ui/settings/PasswordSettingsView.hpp b/src/lms/ui/settings/PasswordSettingsView.hpp new file mode 100644 index 00000000..432bc4b0 --- /dev/null +++ b/src/lms/ui/settings/PasswordSettingsView.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 + +namespace lms::ui +{ + class PasswordSettingsView : public Wt::WContainerWidget + { + public: + PasswordSettingsView(); + + private: + void refreshView(); + }; +} // namespace lms::ui diff --git a/src/lms/ui/settings/ServicesSettingsView.cpp b/src/lms/ui/settings/ServicesSettingsView.cpp new file mode 100644 index 00000000..8fdd2f51 --- /dev/null +++ b/src/lms/ui/settings/ServicesSettingsView.cpp @@ -0,0 +1,176 @@ +/* + * 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 "ServicesSettingsView.hpp" + +#include +#include +#include +#include +#include +#include + +#include "database/Session.hpp" +#include "database/objects/User.hpp" + +#include "LmsApplication.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/ValueStringModel.hpp" + +#include "SettingsViewUtils.hpp" + +namespace lms::ui +{ + namespace + { + class ServicesSettingsModel : public Wt::WFormModel + { + public: + static inline const Field FeedbackBackendField{ "feedback-backend" }; + static inline const Field ScrobblingBackendField{ "scrobbling-backend" }; + static inline const Field ListenBrainzTokenField{ "listenbrainz-token" }; + + using FeedbackBackendModel = ValueStringModel; + using ScrobblingBackendModel = ValueStringModel; + + ServicesSettingsModel() + { + _feedbackBackendModel = std::make_shared(); + _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::FeedbackBackend::Internal); + _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::FeedbackBackend::ListenBrainz); + + _scrobblingBackendModel = std::make_shared(); + _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::ScrobblingBackend::Internal); + _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::ScrobblingBackend::ListenBrainz); + + addField(FeedbackBackendField); + addField(ScrobblingBackendField); + addField(ListenBrainzTokenField); + + setValidator(ListenBrainzTokenField, createMandatoryValidator()); + + loadData(); + } + + std::shared_ptr getFeedbackBackendModel() { return _feedbackBackendModel; } + std::shared_ptr getScrobblingBackendModel() { return _scrobblingBackendModel; } + + void saveData() + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + db::User::pointer user{ LmsApp->getUser() }; + + if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) }) + user.modify()->setFeedbackBackend(_feedbackBackendModel->getValue(*feedbackBackendRow)); + + if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromString(valueText(ScrobblingBackendField)) }) + user.modify()->setScrobblingBackend(_scrobblingBackendModel->getValue(*scrobblingBackendRow)); + + user.modify()->setListenBrainzToken(Wt::asString(value(ListenBrainzTokenField)).toUTF8()); + } + + void loadData() + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + const db::User::pointer user{ LmsApp->getUser() }; + + if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromValue(user->getFeedbackBackend()) }) + setValue(FeedbackBackendField, _feedbackBackendModel->getString(*feedbackBackendRow)); + + if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) }) + setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow)); + + if (const auto listenBrainzToken{ user->getListenBrainzToken() }; !listenBrainzToken.empty()) + setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken })); + + { + const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz }; + setReadOnly(ServicesSettingsModel::ListenBrainzTokenField, !usesListenBrainz); + validator(ServicesSettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); + } + } + + private: + std::shared_ptr _feedbackBackendModel; + std::shared_ptr _scrobblingBackendModel; + }; + } // namespace + + ServicesSettingsView::ServicesSettingsView() + { + wApp->internalPathChanged().connect(this, [this] { + refreshView(); + }); + + refreshView(); + } + + void ServicesSettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings/services")) + return; + + clear(); + + auto* t{ addNew(Wt::WString::tr("Lms.Settings.services.template")) }; + auto model{ std::make_shared() }; + + Wt::WComboBox* feedbackBackendRaw{}; + { + auto feedbackBackend{ std::make_unique() }; + feedbackBackend->setModel(model->getFeedbackBackendModel()); + feedbackBackendRaw = feedbackBackend.get(); + t->setFormWidget(ServicesSettingsModel::FeedbackBackendField, std::move(feedbackBackend)); + } + + Wt::WComboBox* scrobblingBackendRaw{}; + { + auto scrobblingBackend{ std::make_unique() }; + scrobblingBackend->setModel(model->getScrobblingBackendModel()); + scrobblingBackendRaw = scrobblingBackend.get(); + t->setFormWidget(ServicesSettingsModel::ScrobblingBackendField, std::move(scrobblingBackend)); + } + + auto listenbrainzToken{ std::make_unique() }; + Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() }; + listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password); + t->setFormWidget(ServicesSettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); + + auto listenbrainzTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; + listenbrainzTokenVisibilityBtn->clicked().connect(this, [listenbrainzTokenPtr] { + listenbrainzTokenPtr->setEchoMode(listenbrainzTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); + }); + t->bindWidget("listenbrainz-token-visibility-btn", std::move(listenbrainzTokenVisibilityBtn)); + + auto updateListenBrainzTokenField{ [=] { + const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == db::FeedbackBackend::ListenBrainz + || model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == db::ScrobblingBackend::ListenBrainz }; + model->setReadOnly(ServicesSettingsModel::ListenBrainzTokenField, !enable); + model->validator(ServicesSettingsModel::ListenBrainzTokenField)->setMandatory(enable); + t->updateModel(model.get()); + t->updateView(model.get()); + } }; + + feedbackBackendRaw->activated().connect([=] { updateListenBrainzTokenField(); }); + scrobblingBackendRaw->activated().connect([=] { updateListenBrainzTokenField(); }); + + utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); }); + t->updateView(model.get()); + } +} // namespace lms::ui diff --git a/src/lms/ui/settings/ServicesSettingsView.hpp b/src/lms/ui/settings/ServicesSettingsView.hpp new file mode 100644 index 00000000..51e00503 --- /dev/null +++ b/src/lms/ui/settings/ServicesSettingsView.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 + +namespace lms::ui +{ + class ServicesSettingsView : public Wt::WContainerWidget + { + public: + ServicesSettingsView(); + + private: + void refreshView(); + }; +} // namespace lms::ui diff --git a/src/lms/ui/settings/SettingsView.cpp b/src/lms/ui/settings/SettingsView.cpp new file mode 100644 index 00000000..2d444557 --- /dev/null +++ b/src/lms/ui/settings/SettingsView.cpp @@ -0,0 +1,52 @@ +/* + * 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 "SettingsView.hpp" + +#include "core/IConfig.hpp" +#include "core/Service.hpp" + +#include "AudioSettingsView.hpp" +#include "LmsApplication.hpp" +#include "PasswordSettingsView.hpp" +#include "ServicesSettingsView.hpp" +#include "SubsonicSettingsView.hpp" +#include "UISettingsView.hpp" +#include "common/PathRouter.hpp" + +namespace lms::ui +{ + SettingsView::SettingsView() + { + auto* router{ addNew() }; + + router->add("/settings/ui", Wt::WString::tr("Lms.Settings.user-interface")); + router->add("/settings/audio", Wt::WString::tr("Lms.Settings.audio")); + + if (core::Service::get()->getBool("api-subsonic", true)) + router->add("/settings/subsonic", Wt::WString::tr("Lms.Settings.subsonic-api")); + + router->add("/settings/services", Wt::WString::tr("Lms.Settings.services")); + + if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal) + router->add("/settings/password", Wt::WString::tr("Lms.Settings.change-password")); + + router->activate(); + } +} // namespace lms::ui diff --git a/src/lms/ui/SettingsView.hpp b/src/lms/ui/settings/SettingsView.hpp similarity index 95% rename from src/lms/ui/SettingsView.hpp rename to src/lms/ui/settings/SettingsView.hpp index 3a4c7db4..5f970a94 100644 --- a/src/lms/ui/SettingsView.hpp +++ b/src/lms/ui/settings/SettingsView.hpp @@ -23,14 +23,9 @@ namespace lms::ui { - class SettingsView : public Wt::WContainerWidget { public: SettingsView(); - - private: - void refreshView(); }; - } // namespace lms::ui diff --git a/src/lms/ui/settings/SettingsViewUtils.cpp b/src/lms/ui/settings/SettingsViewUtils.cpp new file mode 100644 index 00000000..fd7a40c5 --- /dev/null +++ b/src/lms/ui/settings/SettingsViewUtils.cpp @@ -0,0 +1,57 @@ +/* + * 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 "SettingsViewUtils.hpp" + +#include + +#include +#include + +#include "LmsApplication.hpp" + +namespace lms::ui::utils +{ + void bindSaveDiscardButtons(Wt::WTemplateFormView* t, Wt::WFormModel* model, const std::function& saveData, const std::function& loadData) + { + Wt::WPushButton* saveBtn{ t->bindWidget("save-btn", std::make_unique(Wt::WString::tr("Lms.save"))) }; + Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))) }; + + saveBtn->clicked().connect([=] { + if (LmsApp->getUserType() == db::UserType::DEMO) + { + LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save")); + return; + } + t->updateModel(model); + if (model->validate()) + { + saveData(); + LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.settings-saved")); + } + t->updateView(model); + }); + + discardBtn->clicked().connect([=] { + loadData(); + model->validate(); + t->updateView(model); + }); + } +} // namespace lms::ui::utils diff --git a/src/lms/ui/settings/SettingsViewUtils.hpp b/src/lms/ui/settings/SettingsViewUtils.hpp new file mode 100644 index 00000000..fe657a4d --- /dev/null +++ b/src/lms/ui/settings/SettingsViewUtils.hpp @@ -0,0 +1,30 @@ +/* + * 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 + +namespace lms::ui::utils +{ + void bindSaveDiscardButtons(Wt::WTemplateFormView* t, Wt::WFormModel* model, const std::function& saveData, const std::function& loadData); +} // namespace lms::ui::utils diff --git a/src/lms/ui/settings/SubsonicSettingsView.cpp b/src/lms/ui/settings/SubsonicSettingsView.cpp new file mode 100644 index 00000000..165d797a --- /dev/null +++ b/src/lms/ui/settings/SubsonicSettingsView.cpp @@ -0,0 +1,234 @@ +/* + * 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 "SubsonicSettingsView.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/IConfig.hpp" +#include "core/Service.hpp" +#include "core/UUID.hpp" + +#include "database/Session.hpp" +#include "database/Types.hpp" +#include "database/objects/User.hpp" +#include "services/auth/IAuthTokenService.hpp" + +#include "LmsApplication.hpp" +#include "Tooltip.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/UUIDValidator.hpp" +#include "common/ValueStringModel.hpp" + +#include "SettingsViewUtils.hpp" + +namespace lms::ui +{ + namespace + { + class SubsonicSettingsModel : public Wt::WFormModel + { + public: + static inline const Field SubsonicTokenField{ "subsonic-token" }; + static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" }; + static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; + static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" }; + static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcoding-output-bitrate" }; + + explicit SubsonicSettingsModel(auth::IAuthTokenService& authTokenService) + : _authTokenService{ authTokenService } + { + _transcodingOutputBitrateModel = std::make_shared>(); + db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) { + _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); + }); + + _transcodingOutputFormatModel = std::make_shared>(); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS); + _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS); + + _subsonicArtistListModeModel = std::make_shared>(); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), db::SubsonicArtistListMode::AllArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), db::SubsonicArtistListMode::ReleaseArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), db::SubsonicArtistListMode::TrackArtists); + + addField(SubsonicTokenField); + addField(SubsonicEnableTranscodingByDefault); + addField(SubsonicTranscodingOutputBitrateField); + addField(SubsonicTranscodingOutputFormatField); + addField(SubsonicArtistListModeField); + + setValidator(SubsonicTokenField, createUUIDValidator()); + setValidator(SubsonicTranscodingOutputBitrateField, createMandatoryValidator()); + setValidator(SubsonicTranscodingOutputFormatField, createMandatoryValidator()); + + loadData(); + } + + std::shared_ptr getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; } + std::shared_ptr getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; } + std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } + + void saveData() + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + db::User::pointer user{ LmsApp->getUser() }; + + const std::string token{ Wt::asString(value(SubsonicTokenField)).toUTF8() }; + if (token.empty()) + { + _authTokenService.clearAuthTokens("subsonic", user->getId()); + } + else + { + bool hasNonMatchingToken{ false }; + bool hasMatchingToken{ false }; + _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { + if (storedToken == token) + hasMatchingToken = true; + else + hasNonMatchingToken = true; + }); + + if (!hasMatchingToken || hasNonMatchingToken) + { + _authTokenService.clearAuthTokens("subsonic", user->getId()); + _authTokenService.createAuthToken("subsonic", user->getId(), token); + } + } + + user.modify()->setSubsonicEnableTranscodingByDefault(Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0); + + auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) }; + if (subsonicTranscodingOutputBitrateRow) + user.modify()->setSubsonicDefaultTranscodingOutputBitrate(_transcodingOutputBitrateModel->getValue(*subsonicTranscodingOutputBitrateRow)); + + auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(SubsonicTranscodingOutputFormatField)) }; + if (subsonicTranscodingOutputFormatRow) + user.modify()->setSubsonicDefaultTranscodintOutputFormat(_transcodingOutputFormatModel->getValue(*subsonicTranscodingOutputFormatRow)); + + auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) }; + if (subsonicArtistListModeRow) + user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); + } + + void loadData() + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + const db::User::pointer user{ LmsApp->getUser() }; + + _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { + if (Wt::asString(value(SubsonicTokenField)).empty()) + setValue(SubsonicTokenField, Wt::WString::fromUTF8(std::string{ storedToken })); + }); + + setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault()); + + auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) }; + if (subsonicTranscodingOutputBitrateRow) + setValue(SubsonicTranscodingOutputBitrateField, _transcodingOutputBitrateModel->getString(*subsonicTranscodingOutputBitrateRow)); + + auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputFormat()) }; + if (subsonicTranscodingOutputFormatRow) + setValue(SubsonicTranscodingOutputFormatField, _transcodingOutputFormatModel->getString(*subsonicTranscodingOutputFormatRow)); + + auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) }; + if (subsonicArtistListModeRow) + setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow)); + } + + private: + auth::IAuthTokenService& _authTokenService; + std::shared_ptr> _transcodingOutputBitrateModel; + std::shared_ptr> _transcodingOutputFormatModel; + std::shared_ptr> _subsonicArtistListModeModel; + }; + } // namespace + + SubsonicSettingsView::SubsonicSettingsView() + { + wApp->internalPathChanged().connect(this, [this] { + refreshView(); + }); + + refreshView(); + } + + void SubsonicSettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings/subsonic")) + return; + + clear(); + + auto* t{ addNew(Wt::WString::tr("Lms.Settings.subsonic.template")) }; + t->setCondition("if-has-subsonic-token-usage", core::Service::get()->getBool("api-subsonic-support-user-password-auth", true)); + + auto model{ std::make_shared(*core::Service::get()) }; + + auto subsonicToken{ std::make_unique() }; + Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() }; + subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password); + subsonicTokenPtr->setReadOnly(true); + t->setFormWidget(SubsonicSettingsModel::SubsonicTokenField, std::move(subsonicToken)); + + auto subsonicTokenRegenBtn{ std::make_unique(Wt::WString::tr("Lms.Settings.regen-token")) }; + subsonicTokenRegenBtn->clicked().connect(this, [subsonicTokenPtr] { + subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(std::string{ core::UUID::generate().getAsString() })); + }); + t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn)); + + auto subsonicTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; + subsonicTokenVisibilityBtn->clicked().connect(this, [subsonicTokenPtr] { + subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); + }); + t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn)); + + auto subsonicTokenDelBtn{ std::make_unique(Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) }; + subsonicTokenDelBtn->clicked().connect(this, [subsonicTokenPtr] { + subsonicTokenPtr->setValueText(""); + }); + t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn)); + + t->setFormWidget(SubsonicSettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique()); + + auto transcodingOutputFormat{ std::make_unique() }; + transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel()); + t->setFormWidget(SubsonicSettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodingOutputFormat)); + + auto transcodingOutputBitrate{ std::make_unique() }; + transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel()); + t->setFormWidget(SubsonicSettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodingOutputBitrate)); + + auto artistListMode{ std::make_unique() }; + artistListMode->setModel(model->getSubsonicArtistListModeModel()); + t->setFormWidget(SubsonicSettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); + + utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); }); + t->updateView(model.get()); + initTooltipsForWidgetTree(*t); + } +} // namespace lms::ui diff --git a/src/lms/ui/settings/SubsonicSettingsView.hpp b/src/lms/ui/settings/SubsonicSettingsView.hpp new file mode 100644 index 00000000..52e77176 --- /dev/null +++ b/src/lms/ui/settings/SubsonicSettingsView.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 + +namespace lms::ui +{ + class SubsonicSettingsView : public Wt::WContainerWidget + { + public: + SubsonicSettingsView(); + + private: + void refreshView(); + }; +} // namespace lms::ui diff --git a/src/lms/ui/settings/UISettingsView.cpp b/src/lms/ui/settings/UISettingsView.cpp new file mode 100644 index 00000000..ed70ef6d --- /dev/null +++ b/src/lms/ui/settings/UISettingsView.cpp @@ -0,0 +1,217 @@ +/* + * 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 "UISettingsView.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "core/String.hpp" + +#include "database/Session.hpp" +#include "database/objects/User.hpp" + +#include "LmsApplication.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/ValueStringModel.hpp" + +#include "SettingsViewUtils.hpp" + +namespace lms::ui +{ + namespace + { + // highly unefficient hack to make WSelectionBox work with Wt::WFormModel + class SelectionBox : public Wt::WSelectionBox + { + public: + static inline constexpr std::string_view valueSeparator{ ", " }; + + private: + void setValueText(const Wt::WString& values) override + { + std::set selectedIndexes; + const std::string strValues{ values.toUTF8() }; + for (std::string_view value : core::stringUtils::splitString(strValues, valueSeparator)) + { + const int index{ findText(std::string{ value }) }; + if (index >= 0) + selectedIndexes.insert(index); + } + + setSelectedIndexes(selectedIndexes); + } + + Wt::WString valueText() const override + { + Wt::WString res; + for (int index : selectedIndexes()) + { + if (!res.empty()) + res += std::string{ valueSeparator }; + res += itemText(index); + } + + return res; + } + }; + + class UISettingsModel : public Wt::WFormModel + { + public: + static inline const Field ArtistReleaseSortMethodField{ "artist-release-sort-method" }; + static inline const Field EnableInlineArtistRelationships{ "enable-inline-artist-relationships" }; + static inline const Field InlineArtistRelationships{ "inline-artist-relationships" }; + + using ArtistReleaseSortMethodModel = ValueStringModel; + using ArtistRelationshipsModel = ValueStringModel; + + UISettingsModel() + { + _artistReleaseSortMethodModel = std::make_shared(); + _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-asc"), db::ReleaseSortMethod::DateAsc); + _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-desc"), db::ReleaseSortMethod::DateDesc); + _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-asc"), db::ReleaseSortMethod::OriginalDate); + _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-desc"), db::ReleaseSortMethod::OriginalDateDesc); + _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name); + + _artistRelationshipsModel = std::make_shared(); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.composer", 2), db::TrackArtistLinkType::Composer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.conductor", 2), db::TrackArtistLinkType::Conductor); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.lyricist", 2), db::TrackArtistLinkType::Lyricist); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.mixer", 2), db::TrackArtistLinkType::Mixer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.performer", 2), db::TrackArtistLinkType::Performer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.producer", 2), db::TrackArtistLinkType::Producer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.remixer", 2), db::TrackArtistLinkType::Remixer); + + addField(ArtistReleaseSortMethodField); + addField(EnableInlineArtistRelationships); + addField(InlineArtistRelationships); + + setValidator(ArtistReleaseSortMethodField, createMandatoryValidator()); + + loadData(); + } + + std::shared_ptr getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; } + std::shared_ptr getArtistRelationshipsModel() { return _artistRelationshipsModel; } + + void saveData() + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + db::User::pointer user{ LmsApp->getUser() }; + + const auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) }; + if (artistReleaseSortMethodRow) + user.modify()->setUIArtistReleaseSortMethod(_artistReleaseSortMethodModel->getValue(*artistReleaseSortMethodRow)); + + const bool enableInlineArtistRelationships{ Wt::asNumber(value(EnableInlineArtistRelationships)) != 0 }; + user.modify()->setUIEnableInlineArtistRelationships(enableInlineArtistRelationships); + + core::EnumSet artistLinkTypes; + const std::string relationships{ valueText(InlineArtistRelationships).toUTF8() }; + for (std::string_view relationship : core::stringUtils::splitString(relationships, SelectionBox::valueSeparator)) + { + auto artistRelationshipRow{ _artistRelationshipsModel->getRowFromString(Wt::WString{ std::string{ relationship } }) }; + if (artistRelationshipRow) + artistLinkTypes.insert(_artistRelationshipsModel->getValue(*artistRelationshipRow)); + } + user.modify()->setUIInlineArtistRelationships(artistLinkTypes); + } + + void loadData() + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + const db::User::pointer user{ LmsApp->getUser() }; + + auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) }; + if (artistReleaseSortMethodRow) + setValue(ArtistReleaseSortMethodField, _artistReleaseSortMethodModel->getString(*artistReleaseSortMethodRow)); + + setValue(EnableInlineArtistRelationships, user->getUIEnableInlineArtistRelationships()); + setReadOnly(InlineArtistRelationships, !user->getUIEnableInlineArtistRelationships()); + + Wt::WString inlineArtistRelationships; + for (db::TrackArtistLinkType artistLinkType : user->getUIInlineArtistRelationships()) + { + if (auto artistRelationshipsRow{ _artistRelationshipsModel->getRowFromValue(artistLinkType) }) + { + if (!inlineArtistRelationships.empty()) + inlineArtistRelationships += std::string{ SelectionBox::valueSeparator }; + inlineArtistRelationships += _artistRelationshipsModel->getString(*artistRelationshipsRow); + } + } + setValue(InlineArtistRelationships, inlineArtistRelationships); + } + + private: + std::shared_ptr _artistReleaseSortMethodModel; + std::shared_ptr _artistRelationshipsModel; + }; + } // namespace + + UISettingsView::UISettingsView() + { + wApp->internalPathChanged().connect(this, [this] { + refreshView(); + }); + + refreshView(); + } + + void UISettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings/ui")) + return; + + clear(); + + auto* t{ addNew(Wt::WString::tr("Lms.Settings.ui.template")) }; + auto model{ std::make_shared() }; + + auto enableInlineArtistRelationships{ std::make_unique() }; + auto inlineArtistRelationships{ std::make_unique() }; + inlineArtistRelationships->setSelectionMode(Wt::SelectionMode::Extended); + inlineArtistRelationships->setVerticalSize(3); + inlineArtistRelationships->setModel(model->getArtistRelationshipsModel()); + + auto updateInlineArtistRelationships{ [=](bool readOnly) { + model->setReadOnly(UISettingsModel::InlineArtistRelationships, readOnly); + t->updateModel(model.get()); + t->updateView(model.get()); + } }; + enableInlineArtistRelationships->checked().connect([=] { updateInlineArtistRelationships(false); }); + enableInlineArtistRelationships->unChecked().connect([=] { updateInlineArtistRelationships(true); }); + + auto artistReleaseSortMethod{ std::make_unique() }; + artistReleaseSortMethod->setModel(model->getArtistReleaseSortMethodModel()); + t->setFormWidget(UISettingsModel::ArtistReleaseSortMethodField, std::move(artistReleaseSortMethod)); + t->setFormWidget(UISettingsModel::EnableInlineArtistRelationships, std::move(enableInlineArtistRelationships)); + t->setFormWidget(UISettingsModel::InlineArtistRelationships, std::move(inlineArtistRelationships)); + + utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); }); + t->updateView(model.get()); + } +} // namespace lms::ui diff --git a/src/lms/ui/settings/UISettingsView.hpp b/src/lms/ui/settings/UISettingsView.hpp new file mode 100644 index 00000000..637ef1b1 --- /dev/null +++ b/src/lms/ui/settings/UISettingsView.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 + +namespace lms::ui +{ + class UISettingsView : public Wt::WContainerWidget + { + public: + UISettingsView(); + + private: + void refreshView(); + }; +} // namespace lms::ui