Split settings in several sub views. easier to understand that save is needed, ref #796

This commit is contained in:
emeric
2026-06-02 23:24:44 +02:00
parent 5b4eb8db8b
commit 578b869096
34 changed files with 1904 additions and 1069 deletions
+10 -1
View File
@@ -58,7 +58,16 @@
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li class="dropdown-header">${username}</li>
<li>${settings class="dropdown-item"}</li>
<li>${settings-ui class="dropdown-item"}</li>
<li>${settings-audio class="dropdown-item"}</li>
${<if-has-subsonic-api-menu>}
<li>${settings-subsonic class="dropdown-item"}</li>
${</if-has-subsonic-api-menu>}
<li>${settings-services class="dropdown-item"}</li>
${<if-has-change-password-menu>}
<li>${settings-password class="dropdown-item"}</li>
${</if-has-change-password-menu>}
<li class="dropdown-divider"></li>
<li>${logout class="dropdown-item"}</li>
</ul>
</li>
+5 -1
View File
@@ -354,7 +354,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">Enable transcoding by default</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Types of artist relationships to display</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> User Interface</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> Audio</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> Subsonic API</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> Services</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> Change password</message>
<message id="Lms.Settings.name">Album Name</message>
<message id="Lms.Settings.original-date-asc">Original Release Date (Ascending)</message>
<message id="Lms.Settings.original-date-desc">Original Release Date (Descending)</message>
+5 -1
View File
@@ -354,7 +354,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">Activar por defecto la transcodificación</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Tipos de relaciones entre artistas a mostrar</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ajustes</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> Interfaz de usuario</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> Audio</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> API Subsonic</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> Servicios</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> Cambiar contraseña</message>
<message id="Lms.Settings.name">Nombre del álbum</message>
<message id="Lms.Settings.original-date-asc">Fecha de publicación original (Ascendiente)</message>
<message id="Lms.Settings.original-date-desc">Fecha de publicación original (Descendiente)</message>
+5 -1
View File
@@ -354,7 +354,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">Activer par défaut le transcodage</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Types de relations d'artiste à afficher</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> Interface utilisateur</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> Audio</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> API Subsonic</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> Services</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> Changement de mot de passe</message>
<message id="Lms.Settings.name">Nom de l'album</message>
<message id="Lms.Settings.original-date-asc">Date de sortie originale (Croissant)</message>
<message id="Lms.Settings.original-date-desc">Date de sortie originale (Décroissant)</message>
+5 -1
View File
@@ -354,7 +354,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">Abilita transcodifica di default</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Tipi di relazioni artistiche da visualizzare</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> Interfaccia utente</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> Audio</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> API Subsonic</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> Servizi</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> Cambia password</message>
<message id="Lms.Settings.name">Nome dell'album</message>
<message id="Lms.Settings.original-date-asc">Data di uscita originale (Crescente)</message>
<message id="Lms.Settings.original-date-desc">Data di uscita originale (Decrescente)</message>
+5 -1
View File
@@ -380,7 +380,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">Aktywuj domyślnie transkodowanie</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Typy powiązań artystów do wyświetlenia</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ustawienia</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> Interfejs użytkownika</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> Dźwięk</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> API Subsonic</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> Usługi</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> Zmień hasło</message>
<message id="Lms.Settings.name">Nazwa albumu</message>
<message id="Lms.Settings.original-date-asc">Oryginalna data wydania (Rosnąco)</message>
<message id="Lms.Settings.original-date-desc">Oryginalna data wydania (Malejąco)</message>
+5 -1
View File
@@ -325,7 +325,11 @@
<message id="Lms.Settings.enable-transcoding-by-default">默认启用转码</message>
<message id="Lms.Settings.feedback">反馈</message>
<message id="Lms.Settings.inline-artist-relationships">要显示的艺术家关系类型</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.menu-ui"><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> 用户界面</message>
<message id="Lms.Settings.menu-audio"><i class="fa fa-fw fa-music" aria-hidden="true"></i> 音频</message>
<message id="Lms.Settings.menu-subsonic"><i class="fa fa-fw fa-key" aria-hidden="true"></i> Subsonic 应用程序接口</message>
<message id="Lms.Settings.menu-services"><i class="fa fa-fw fa-exchange" aria-hidden="true"></i> 服务</message>
<message id="Lms.Settings.menu-password"><i class="fa fa-fw fa-lock" aria-hidden="true"></i> 更改密码</message>
<message id="Lms.Settings.name">专辑名称</message>
<message id="Lms.Settings.original-date-asc">原始发行日期(升序)</message>
<message id="Lms.Settings.original-date-desc">原始发行日期(降序)</message>
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Settings.audio.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.audio}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.audio-settings-are-local}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button>
</legend>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-mode}">
${tr:Lms.Settings.transcoding-mode}
</label>
${transcoding-mode class="form-control"}
<div class="invalid-feedback">
${transcoding-mode-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-output-format}">
${tr:Lms.Settings.transcoding-output-format}
</label>
${transcoding-output-format class="form-control"}
<div class="invalid-feedback">
${transcoding-output-format-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-output-bitrate}">
${tr:Lms.Settings.transcoding-output-bitrate}
</label>
<div class="input-group mb-3">
${transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span>
</div>
<div class="invalid-feedback">
${transcoding-output-bitrate-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-mode}">
${tr:Lms.Settings.replaygain-mode}
</label>
${replaygain-mode class="form-control"}
<div class="invalid-feedback">
${replaygain-mode-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-preamp}">
${tr:Lms.Settings.replaygain-preamp}
</label>
<div class="input-group mb-3">
${replaygain-preamp class="form-control"}
<span class="input-group-text">dB</span>
<div class="invalid-feedback">
${replaygain-preamp-info}
</div>
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-preamp-no-rg-info}">
${tr:Lms.Settings.replaygain-preamp-no-rg-info}
</label>
<div class="input-group mb-3">
${replaygain-preamp-no-rg-info class="form-control"}
<span class="input-group-text">dB</span>
<div class="invalid-feedback">
${replaygain-preamp-no-rg-info-info}
</div>
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Settings.password.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.change-password}</legend>
${<if-has-old-password>}
<div class="col-12">
<label class="form-label" for="${id:password-old}">
${tr:Lms.password-old}
</label>
${password-old class="form-control"}
<div class="invalid-feedback">
${password-old-info class="help-block"}
</div>
</div>
${</if-has-old-password>}
<div class="col-lg-6">
<label class="form-label" for="${id:password}">
${tr:Lms.password-new}
</label>
${password class="form-control"}
<div class="invalid-feedback">
${password-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
${password-confirm class="form-control"}
<div class="invalid-feedback">
${password-confirm-info class="help-block"}
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Settings.services.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.services}</legend>
<div class="col-lg-6">
<label class="form-label" for="${id:scrobbling-backend}">
${tr:Lms.Settings.scrobbling}
</label>
${scrobbling-backend class="form-control"}
<div class="invalid-feedback">
${scrobbling-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:feedback-backend}">
${tr:Lms.Settings.feedback}
</label>
${feedback-backend class="form-control"}
<div class="invalid-feedback">
${feedback-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-12">
<label class="form-label" for="${id:listenbrainz-token}">
${tr:Lms.Settings.backend.listenbrainz-token}
</label>
<div class="input-group mb-3">
${listenbrainz-token class="form-control"}
${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"}
<div class="invalid-feedback">
${listenbrainz-token-info class="help-block"}
</div>
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Settings.subsonic.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="col-lg-12">
<label class="form-label" for="${id:subsonic-token}">
${tr:Lms.Settings.subsonic-token}
${<if-has-subsonic-token-usage>}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.subsonic-token-usage}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button>
${</if-has-subsonic-token-usage>}
</label>
<div class="input-group mb-3">
${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"}
<div class="invalid-feedback">
${subsonic-token-info class="help-block"}
</div>
</div>
</div>
<div class="col-12">
<div class="form-check">
${subsonic-enable-transcoding-by-default class="form-check-input"}
<label class="form-check-label" for="${id:subsonic-enable-transcoding-by-default}">
${tr:Lms.Settings.enable-transcoding-by-default}
</label>
<div class="invalid-feedback">
${subsonic-enable-transcoding-by-default-info}
</div>
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcoding-output-format}">
${tr:Lms.Settings.default-transcoding-output-format}
</label>
${subsonic-transcoding-output-format class="form-control"}
<div class="invalid-feedback">
${subsonic-transcoding-output-format-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcoding-output-bitrate}">
${tr:Lms.Settings.default-transcoding-output-bitrate}
</label>
<div class="input-group mb-3">
${subsonic-transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span>
</div>
<div class="invalid-feedback">
${subsonic-transcoding-output-bitrate-info class="help-block"}
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:subsonic-artist-list-mode}">
${tr:Lms.Settings.subsonic-artist-list-mode}
</label>
${subsonic-artist-list-mode class="form-control"}
<div class="invalid-feedback">
${subsonic-artist-list-mode-info class="help-block"}
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Settings.ui.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.user-interface}</legend>
<div class="col-12">
<label class="form-label" for="${id:artist-release-sort-method}">
${tr:Lms.Settings.artist-release-sort-method}
</label>
${artist-release-sort-method class="form-control"}
<div class="invalid-feedback">
${artist-release-sort-method-info}
</div>
</div>
<div class="col-12">
<div class="form-check">
${enable-inline-artist-relationships class="form-check-input"}
<label class="form-check-label" for="${id:enable-inline-artist-relationships}">
${tr:Lms.Settings.enable-inline-artist-relationships}
</label>
<div class="invalid-feedback">
${enable-inline-artist-relationships}
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:inline-artist-relationships}">
${tr:Lms.Settings.inline-artist-relationships}
</label>
${inline-artist-relationships class="form-control"}
<div class="invalid-feedback">
${inline-artist-relationships-info}
</div>
</div>
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
-237
View File
@@ -1,237 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Settings.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.user-interface}</legend>
<div class="col-12">
<label class="form-label" for="${id:artist-release-sort-method}">
${tr:Lms.Settings.artist-release-sort-method}
</label>
${artist-release-sort-method class="form-control"}
<div class="invalid-feedback">
${artist-release-sort-method-info}
</div>
</div>
<div class="col-12">
<div class="form-check">
${enable-inline-artist-relationships class="form-check-input"}
<label class="form-check-label" for="${id:enable-inline-artist-relationships}">
${tr:Lms.Settings.enable-inline-artist-relationships}
</label>
<div class="invalid-feedback">
${enable-inline-artist-relationships}
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:inline-artist-relationships}">
${tr:Lms.Settings.inline-artist-relationships}
</label>
${inline-artist-relationships class="form-control"}
<div class="invalid-feedback">
${inline-artist-relationships-info}
</div>
</div>
<legend>${tr:Lms.Settings.audio}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.audio-settings-are-local}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button>
</legend>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-mode}">
${tr:Lms.Settings.transcoding-mode}
</label>
${transcoding-mode class="form-control"}
<div class="invalid-feedback">
${transcoding-mode-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-output-format}">
${tr:Lms.Settings.transcoding-output-format}
</label>
${transcoding-output-format class="form-control"}
<div class="invalid-feedback">
${transcoding-output-format-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:transcoding-output-bitrate}">
${tr:Lms.Settings.transcoding-output-bitrate}
</label>
<div class="input-group mb-3">
${transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span>
</div>
<div class="invalid-feedback">
${transcoding-output-bitrate-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-mode}">
${tr:Lms.Settings.replaygain-mode}
</label>
${replaygain-mode class="form-control"}
<div class="invalid-feedback">
${replaygain-mode-info}
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-preamp}">
${tr:Lms.Settings.replaygain-preamp}
</label>
<div class="input-group mb-3">
${replaygain-preamp class="form-control"}
<span class="input-group-text">dB</span>
<div class="invalid-feedback">
${replaygain-preamp-info}
</div>
</div>
</div>
<div class="col-lg-4">
<label class="form-label" for="${id:replaygain-preamp-no-rg-info}">
${tr:Lms.Settings.replaygain-preamp-no-rg-info}
</label>
<div class="input-group mb-3">
${replaygain-preamp-no-rg-info class="form-control"}
<span class="input-group-text">dB</span>
<div class="invalid-feedback">
${replaygain-preamp-no-rg-info-info}
</div>
</div>
</div>
${<if-has-subsonic-api>}
<legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="col-lg-12">
<label class="form-label" for="${id:subsonic-token}">
${tr:Lms.Settings.subsonic-token}
${<if-has-subsonic-token-usage>}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.subsonic-token-usage}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button>
${</if-has-subsonic-token-usage>}
</label>
<div class="input-group mb-3">
${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"}
<div class="invalid-feedback">
${subsonic-token-info class="help-block"}
</div>
</div>
</div>
<div class="col-12">
<div class="form-check">
${subsonic-enable-transcoding-by-default class="form-check-input"}
<label class="form-check-label" for="${id:subsonic-enable-transcoding-by-default}">
${tr:Lms.Settings.enable-transcoding-by-default}
</label>
<div class="invalid-feedback">
${subsonic-enable-transcoding-by-default-info}
</div>
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcoding-output-format}">
${tr:Lms.Settings.default-transcoding-output-format}
</label>
${subsonic-transcoding-output-format class="form-control"}
<div class="invalid-feedback">
${subsonic-transcoding-output-format-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcoding-output-bitrate}">
${tr:Lms.Settings.default-transcoding-output-bitrate}
</label>
<div class="input-group mb-3">
${subsonic-transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span>
</div>
<div class="invalid-feedback">
${subsonic-transcoding-output-bitrate-info class="help-block"}
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:subsonic-artist-list-mode}">
${tr:Lms.Settings.subsonic-artist-list-mode}
</label>
${subsonic-artist-list-mode class="form-control"}
<div class="invalid-feedback">
${subsonic-artist-list-mode-info class="help-block"}
</div>
</div>
${</if-has-subsonic-api>}
<legend>${tr:Lms.Settings.services}</legend>
<div class="col-lg-6">
<label class="form-label" for="${id:scrobbling-backend}">
${tr:Lms.Settings.scrobbling}
</label>
${scrobbling-backend class="form-control"}
<div class="invalid-feedback">
${scrobbling-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:feedback-backend}">
${tr:Lms.Settings.feedback}
</label>
${feedback-backend class="form-control"}
<div class="invalid-feedback">
${feedback-backend-info class="help-block"}
</div>
</div>
<div class="col-lg-12">
<label class="form-label" for="${id:listenbrainz-token}">
${tr:Lms.Settings.backend.listenbrainz-token}
</label>
<div class="input-group mb-3">
${listenbrainz-token class="form-control"}
${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"}
<div class="invalid-feedback">
${listenbrainz-token-info class="help-block"}
</div>
</div>
</div>
${<if-has-change-password>}
<legend>${tr:Lms.Settings.change-password}</legend>
${<if-has-old-password>}
<div class="col-12">
<label class="form-label" for="${id:password-old}">
${tr:Lms.password-old}
</label>
${password-old class="form-control"}
<div class="invalid-feedback">
${password-old-info class="help-block"}
</div>
</div>
${</if-has-old-password>}
<div class="col-lg-6">
<label class="form-label" for="${id:password}">
${tr:Lms.password-new}
</label>
${password class="form-control"}
<div class="invalid-feedback">
${password-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
${password-confirm class="form-control"}
<div class="invalid-feedback">
${password-confirm-info class="help-block"}
</div>
</div>
${</if-has-change-password>}
</form>
<hr/>
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message>
</messages>
+9 -1
View File
@@ -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
+26 -30
View File
@@ -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>("filters") };
navbar->bindString("username", std::string{ getUserLoginName() }, Wt::TextFormat::Plain);
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings"));
navbar->bindNew<Wt::WAnchor>("settings-ui", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/ui" }, Wt::WString::tr("Lms.Settings.menu-ui"));
navbar->bindNew<Wt::WAnchor>("settings-audio", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/audio" }, Wt::WString::tr("Lms.Settings.menu-audio"));
if (core::Service<core::IConfig>::get()->getBool("api-subsonic", true))
{
navbar->setCondition("if-has-subsonic-api-menu", true);
navbar->bindNew<Wt::WAnchor>("settings-subsonic", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/subsonic" }, Wt::WString::tr("Lms.Settings.menu-subsonic"));
}
navbar->bindNew<Wt::WAnchor>("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<Wt::WAnchor>("settings-password", Wt::WLink{ Wt::LinkType::InternalPath, "/settings/password" }, Wt::WString::tr("Lms.Settings.menu-password"));
}
{
Wt::WAnchor* logout{ navbar->bindNew<Wt::WAnchor>("logout") };
@@ -499,14 +502,7 @@ namespace lms::ui
// Admin stuff
if (getUserType() == db::UserType::ADMIN)
{
mainStack->addNew<MediaLibrariesView>();
mainStack->addNew<ScanSettingsView>();
mainStack->addNew<ScannerController>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
mainStack->addNew<DebugToolsView>();
}
mainStack->addNew<AdminView>();
explore->getPlayQueueController().setMaxTrackCountToEnqueue(_playQueue->getCapacity());
-789
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "SettingsView.hpp"
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WDoubleSpinBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/WSelectionBox.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<int> 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<db::ReleaseSortMethod>;
using ArtistRelationshipsModel = ValueStringModel<db::TrackArtistLinkType>;
using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcoding::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
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<ArtistReleaseSortMethodModel> getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
std::shared_ptr<ArtistRelationshipsModel> getArtistRelationshipsModel() { return _artistRelationshipsModel; }
std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; }
std::shared_ptr<FeedbackBackendModel> getFeedbackBackendModel() { return _feedbackBackendModel; }
std::shared_ptr<ScrobblingBackendModel> 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<db::TrackArtistLinkType> 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>();
_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>();
_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<TranscodingModeModel>();
_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<ValueStringModel<db::Bitrate>>();
db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) {
_transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
});
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<db::TranscodingOutputFormat>>();
_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>();
_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<ValueStringModel<db::SubsonicArtistListMode>>();
_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<ValueStringModel<db::FeedbackBackend>>();
_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<ValueStringModel<db::ScrobblingBackend>>();
_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> _artistReleaseSortMethodModel;
std::shared_ptr<ArtistRelationshipsModel> _artistRelationshipsModel;
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<db::Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<db::TranscodingOutputFormat>> _transcodingOutputFormatModel;
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
std::shared_ptr<ValueStringModel<db::SubsonicArtistListMode>> _subsonicArtistListModeModel;
std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel;
std::shared_ptr<ScrobblingBackendModel> _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::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template")) };
auth::IPasswordService* authPasswordService{};
if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
{
authPasswordService = core::Service<auth::IPasswordService>::get();
assert(authPasswordService->canSetPasswords());
}
auto model{ std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong(), *core::Service<auth::IAuthTokenService>::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<Wt::WLineEdit>() };
oldPassword->setEchoMode(Wt::EchoMode::Password);
oldPassword->setAttributeValue("autocomplete", "current-password");
t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword));
}
// Password
auto password{ std::make_unique<Wt::WLineEdit>() };
password->setEchoMode(Wt::EchoMode::Password);
password->setAttributeValue("autocomplete", "new-password");
t->setFormWidget(SettingsModel::PasswordField, std::move(password));
// Password confirm
auto passwordConfirm{ std::make_unique<Wt::WLineEdit>() };
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
passwordConfirm->setAttributeValue("autocomplete", "new-password");
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
}
// User interface
{
auto artistReleaseSortMethod{ std::make_unique<Wt::WComboBox>() };
artistReleaseSortMethod->setModel(model->getArtistReleaseSortMethodModel());
t->setFormWidget(SettingsModel::ArtistReleaseSortMethodField, std::move(artistReleaseSortMethod));
auto enableInlineArtistRelationships{ std::make_unique<Wt::WCheckBox>() };
auto inlineArtistRelationships{ std::make_unique<SelectionBox>() };
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<Wt::WComboBox>() };
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<Wt::WComboBox>() };
transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel());
t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodingOutputFormat));
// Bitrate
auto transcodingOutputBitrate{ std::make_unique<Wt::WComboBox>() };
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
// Replay gain mode
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
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<Wt::WDoubleSpinBox>() };
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<Wt::WDoubleSpinBox>() };
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<core::IConfig>::get()->getBool("api-subsonic", true));
t->setCondition("if-has-subsonic-token-usage", core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true));
auto subsonicToken{ std::make_unique<Wt::WLineEdit>() };
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::WPushButton>(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::WPushButton>(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::WPushButton>(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<Wt::WCheckBox>());
// Format
auto transcodingOutputFormat{ std::make_unique<Wt::WComboBox>() };
transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel());
t->setFormWidget(SettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodingOutputFormat));
// Bitrate
auto transcodingOutputBitrate{ std::make_unique<Wt::WComboBox>() };
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodingOutputBitrate));
// Artist list mode
auto artistListMode{ std::make_unique<Wt::WComboBox>() };
artistListMode->setModel(model->getSubsonicArtistListModeModel());
t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode));
}
// Feedback
Wt::WComboBox* feedbackBackendRaw{};
{
auto feedbackBackend{ std::make_unique<Wt::WComboBox>() };
feedbackBackend->setModel(model->getFeedbackBackendModel());
feedbackBackendRaw = feedbackBackend.get();
t->setFormWidget(SettingsModel::FeedbackBackendField, std::move(feedbackBackend));
}
// Scrobbling
Wt::WComboBox* scrobblingBackendRaw;
{
auto scrobblingBackend{ std::make_unique<Wt::WComboBox>() };
scrobblingBackend->setModel(model->getScrobblingBackendModel());
scrobblingBackendRaw = scrobblingBackend.get();
t->setFormWidget(SettingsModel::ScrobblingBackendField, std::move(scrobblingBackend));
}
// Backend settings
{
auto listenbrainzToken{ std::make_unique<Wt::WLineEdit>() };
Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() };
listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken));
auto listenbrainzTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(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::WPushButton>(Wt::WString::tr("Lms.save"))) };
Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(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
+45
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<PathRouter>() };
router->add<MediaLibrariesView>("/admin/libraries", Wt::WString::tr("Lms.Admin.MediaLibraries.media-libraries"));
router->add<ScanSettingsView>("/admin/scan-settings", Wt::WString::tr("Lms.Admin.Database.scan-settings"));
router->add<ScannerController>("/admin/scanner", Wt::WString::tr("Lms.Admin.ScannerController.scanner"));
router->add<UsersView>("/admin/users", Wt::WString::tr("Lms.Admin.Users.users"));
router->add<UserView>("/admin/user", std::nullopt);
router->add<DebugToolsView>("/admin/debug-tools", Wt::WString::tr("Lms.Admin.DebugTools.debug-tools"));
router->activate();
}
} // namespace lms::ui
+31
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class AdminView : public Wt::WContainerWidget
{
public:
AdminView();
};
} // namespace lms::ui
+57
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PathRouter.hpp"
#include <Wt/WApplication.h>
#include "LmsApplication.hpp"
namespace lms::ui
{
PathRouter::PathRouter()
: _stack{ addNew<Wt::WStackedWidget>() }
{
_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
+62
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <Wt/WContainerWidget.h>
#include <Wt/WStackedWidget.h>
#include <Wt/WString.h>
namespace lms::ui
{
class PathRouter : public Wt::WContainerWidget
{
public:
PathRouter();
template<typename T, typename... Args>
T* add(std::string_view path, std::optional<Wt::WString> title, Args&&... args)
{
T* widget{ _stack->addNew<T>(std::forward<Args>(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<Wt::WString> title;
};
std::vector<Route> _routes;
};
} // namespace lms::ui
+243
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "AudioSettingsView.hpp"
#include <Wt/WComboBox.h>
#include <Wt/WDoubleSpinBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<MediaPlayer::Settings::Transcoding::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
AudioSettingsModel()
{
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
_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<ValueStringModel<db::Bitrate>>();
db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) {
_transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
});
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<db::TranscodingOutputFormat>>();
_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>();
_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<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
std::shared_ptr<ReplayGainModeModel> 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<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<db::Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<db::TranscodingOutputFormat>> _transcodingOutputFormatModel;
std::shared_ptr<ReplayGainModeModel> _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::WTemplateFormView>(Wt::WString::tr("Lms.Settings.audio.template")) };
auto model{ std::make_shared<AudioSettingsModel>() };
auto transcodingMode{ std::make_unique<Wt::WComboBox>() };
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<Wt::WComboBox>() };
transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel());
t->setFormWidget(AudioSettingsModel::TranscodeFormatField, std::move(transcodingOutputFormat));
auto transcodingOutputBitrate{ std::make_unique<Wt::WComboBox>() };
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(AudioSettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
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<Wt::WDoubleSpinBox>() };
replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
t->setFormWidget(AudioSettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain));
auto replayGainPreampGainIfNoInfo{ std::make_unique<Wt::WDoubleSpinBox>() };
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
+35
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class AudioSettingsView : public Wt::WContainerWidget
{
public:
AudioSettingsView();
private:
void refreshView();
};
} // namespace lms::ui
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PasswordSettingsView.hpp"
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<auth::IPasswordService>::get() };
if (!authPasswordService)
return;
const bool withOldPassword{ !LmsApp->isUserAuthStrong() };
auto* t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.password.template")) };
if (withOldPassword)
{
t->setCondition("if-has-old-password", true);
auto oldPassword{ std::make_unique<Wt::WLineEdit>() };
oldPassword->setEchoMode(Wt::EchoMode::Password);
oldPassword->setAttributeValue("autocomplete", "current-password");
t->setFormWidget(PasswordSettingsModel::PasswordOldField, std::move(oldPassword));
}
auto password{ std::make_unique<Wt::WLineEdit>() };
password->setEchoMode(Wt::EchoMode::Password);
password->setAttributeValue("autocomplete", "new-password");
t->setFormWidget(PasswordSettingsModel::PasswordField, std::move(password));
auto passwordConfirm{ std::make_unique<Wt::WLineEdit>() };
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
passwordConfirm->setAttributeValue("autocomplete", "new-password");
t->setFormWidget(PasswordSettingsModel::PasswordConfirmField, std::move(passwordConfirm));
auto model{ std::make_shared<PasswordSettingsModel>(*authPasswordService, withOldPassword, *core::Service<auth::IAuthTokenService>::get()) };
utils::bindSaveDiscardButtons(t, model.get(), [model] { model->saveData(); }, [model] { model->loadData(); });
t->updateView(model.get());
}
} // namespace lms::ui
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class PasswordSettingsView : public Wt::WContainerWidget
{
public:
PasswordSettingsView();
private:
void refreshView();
};
} // namespace lms::ui
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ServicesSettingsView.hpp"
#include <Wt/WComboBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<db::FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
ServicesSettingsModel()
{
_feedbackBackendModel = std::make_shared<FeedbackBackendModel>();
_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>();
_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<FeedbackBackendModel> getFeedbackBackendModel() { return _feedbackBackendModel; }
std::shared_ptr<ScrobblingBackendModel> 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> _feedbackBackendModel;
std::shared_ptr<ScrobblingBackendModel> _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::WTemplateFormView>(Wt::WString::tr("Lms.Settings.services.template")) };
auto model{ std::make_shared<ServicesSettingsModel>() };
Wt::WComboBox* feedbackBackendRaw{};
{
auto feedbackBackend{ std::make_unique<Wt::WComboBox>() };
feedbackBackend->setModel(model->getFeedbackBackendModel());
feedbackBackendRaw = feedbackBackend.get();
t->setFormWidget(ServicesSettingsModel::FeedbackBackendField, std::move(feedbackBackend));
}
Wt::WComboBox* scrobblingBackendRaw{};
{
auto scrobblingBackend{ std::make_unique<Wt::WComboBox>() };
scrobblingBackend->setModel(model->getScrobblingBackendModel());
scrobblingBackendRaw = scrobblingBackend.get();
t->setFormWidget(ServicesSettingsModel::ScrobblingBackendField, std::move(scrobblingBackend));
}
auto listenbrainzToken{ std::make_unique<Wt::WLineEdit>() };
Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() };
listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(ServicesSettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken));
auto listenbrainzTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class ServicesSettingsView : public Wt::WContainerWidget
{
public:
ServicesSettingsView();
private:
void refreshView();
};
} // namespace lms::ui
+52
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<PathRouter>() };
router->add<UISettingsView>("/settings/ui", Wt::WString::tr("Lms.Settings.user-interface"));
router->add<AudioSettingsView>("/settings/audio", Wt::WString::tr("Lms.Settings.audio"));
if (core::Service<core::IConfig>::get()->getBool("api-subsonic", true))
router->add<SubsonicSettingsView>("/settings/subsonic", Wt::WString::tr("Lms.Settings.subsonic-api"));
router->add<ServicesSettingsView>("/settings/services", Wt::WString::tr("Lms.Settings.services"));
if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
router->add<PasswordSettingsView>("/settings/password", Wt::WString::tr("Lms.Settings.change-password"));
router->activate();
}
} // namespace lms::ui
@@ -23,14 +23,9 @@
namespace lms::ui
{
class SettingsView : public Wt::WContainerWidget
{
public:
SettingsView();
private:
void refreshView();
};
} // namespace lms::ui
+57
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "SettingsViewUtils.hpp"
#include <functional>
#include <Wt/WPushButton.h>
#include <Wt/WString.h>
#include "LmsApplication.hpp"
namespace lms::ui::utils
{
void bindSaveDiscardButtons(Wt::WTemplateFormView* t, Wt::WFormModel* model, const std::function<void()>& saveData, const std::function<void()>& loadData)
{
Wt::WPushButton* saveBtn{ t->bindWidget("save-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.save"))) };
Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(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
+30
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <Wt/WFormModel.h>
#include <Wt/WTemplateFormView.h>
namespace lms::ui::utils
{
void bindSaveDiscardButtons(Wt::WTemplateFormView* t, Wt::WFormModel* model, const std::function<void()>& saveData, const std::function<void()>& loadData);
} // namespace lms::ui::utils
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "SubsonicSettingsView.hpp"
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<ValueStringModel<db::Bitrate>>();
db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) {
_transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
});
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<db::TranscodingOutputFormat>>();
_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<ValueStringModel<db::SubsonicArtistListMode>>();
_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<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
std::shared_ptr<Wt::WAbstractItemModel> 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<ValueStringModel<db::Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<db::TranscodingOutputFormat>> _transcodingOutputFormatModel;
std::shared_ptr<ValueStringModel<db::SubsonicArtistListMode>> _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::WTemplateFormView>(Wt::WString::tr("Lms.Settings.subsonic.template")) };
t->setCondition("if-has-subsonic-token-usage", core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true));
auto model{ std::make_shared<SubsonicSettingsModel>(*core::Service<auth::IAuthTokenService>::get()) };
auto subsonicToken{ std::make_unique<Wt::WLineEdit>() };
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::WPushButton>(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::WPushButton>(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::WPushButton>(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<Wt::WCheckBox>());
auto transcodingOutputFormat{ std::make_unique<Wt::WComboBox>() };
transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel());
t->setFormWidget(SubsonicSettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodingOutputFormat));
auto transcodingOutputBitrate{ std::make_unique<Wt::WComboBox>() };
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SubsonicSettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodingOutputBitrate));
auto artistListMode{ std::make_unique<Wt::WComboBox>() };
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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class SubsonicSettingsView : public Wt::WContainerWidget
{
public:
SubsonicSettingsView();
private:
void refreshView();
};
} // namespace lms::ui
+217
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "UISettingsView.hpp"
#include <set>
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WSelectionBox.h>
#include <Wt/WString.h>
#include <Wt/WTemplateFormView.h>
#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<int> 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<db::ReleaseSortMethod>;
using ArtistRelationshipsModel = ValueStringModel<db::TrackArtistLinkType>;
UISettingsModel()
{
_artistReleaseSortMethodModel = std::make_shared<ArtistReleaseSortMethodModel>();
_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>();
_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<ArtistReleaseSortMethodModel> getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
std::shared_ptr<ArtistRelationshipsModel> 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<db::TrackArtistLinkType> 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> _artistReleaseSortMethodModel;
std::shared_ptr<ArtistRelationshipsModel> _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::WTemplateFormView>(Wt::WString::tr("Lms.Settings.ui.template")) };
auto model{ std::make_shared<UISettingsModel>() };
auto enableInlineArtistRelationships{ std::make_unique<Wt::WCheckBox>() };
auto inlineArtistRelationships{ std::make_unique<SelectionBox>() };
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<Wt::WComboBox>() };
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
+34
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WContainerWidget.h>
namespace lms::ui
{
class UISettingsView : public Wt::WContainerWidget
{
public:
UISettingsView();
private:
void refreshView();
};
} // namespace lms::ui