Split settings in several sub views. easier to understand that save is needed, ref #796
This commit is contained in:
+10
-1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user