- ${tr:Lms.Settings.audio-settings-are-local}
+
${tr:Lms.Settings.user-interface}
+
+
+ ${tr:Lms.Settings.artist-release-sort-method}
+
+ ${artist-release-sort-method class="form-control"}
+
+ ${artist-release-sort-method-info}
+
+
${tr:Lms.Settings.audio}
+
+
+
+
${tr:Lms.Settings.transcoding-mode}
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index 6c9563c2..57e03d33 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
- static constexpr Version LMS_DATABASE_VERSION{ 69 };
+ static constexpr Version LMS_DATABASE_VERSION{ 70 };
}
VersionInfo::VersionInfo()
@@ -872,6 +872,12 @@ SELECT
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
+ void migrateFromV69(Session& session)
+ {
+ // Add a field in UI settings
+ session.getDboSession()->execute("ALTER TABLE user ADD COLUMN ui_artist_release_sort_method NOT NULL DEFAULT 7"); // 7 = ReleaseSortMethod::OriginalDateDesc
+ }
+
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -917,6 +923,7 @@ SELECT
{ 66, migrateFromV66 },
{ 67, migrateFromV67 },
{ 68, migrateFromV68 },
+ { 69, migrateFromV69 },
};
bool migrationPerformed{};
diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp
index f1ea2cb3..cbf77810 100644
--- a/src/libs/database/include/database/User.hpp
+++ b/src/libs/database/include/database/User.hpp
@@ -75,6 +75,7 @@ namespace lms::db
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
+ static inline constexpr ReleaseSortMethod _defaultUIArtistReleaseSortMethod{ ReleaseSortMethod::OriginalDateDesc };
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
@@ -106,6 +107,7 @@ namespace lms::db
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
+ void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
@@ -120,6 +122,7 @@ namespace lms::db
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
UITheme getUITheme() const { return _uiTheme; }
+ ReleaseSortMethod getUIArtistReleaseSortMethod() const { return _uiArtistReleaseSortMethod; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
@@ -138,6 +141,7 @@ namespace lms::db
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
+ Wt::Dbo::field(a, _uiArtistReleaseSortMethod, "ui_artist_release_sort_method");
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
@@ -156,6 +160,7 @@ namespace lms::db
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme };
+ ReleaseSortMethod _uiArtistReleaseSortMethod{ _defaultUIArtistReleaseSortMethod };
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
std::string _listenbrainzToken; // Musicbrainz Identifier
@@ -172,5 +177,4 @@ namespace lms::db
Wt::Dbo::collection> _authTokens;
Wt::Dbo::collection> _uiStates;
};
-
} // namespace lms::db
diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt
index 6a30b407..55fc992e 100644
--- a/src/lms/CMakeLists.txt
+++ b/src/lms/CMakeLists.txt
@@ -12,6 +12,7 @@ add_executable(lms
ui/PlayQueue.cpp
ui/SettingsView.cpp
ui/State.cpp
+ ui/Tooltip.cpp
ui/Utils.cpp
ui/admin/InitWizardView.cpp
ui/admin/MediaLibrariesView.cpp
diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp
index 27ba0247..6e3c91ea 100644
--- a/src/lms/ui/SettingsView.cpp
+++ b/src/lms/ui/SettingsView.cpp
@@ -37,6 +37,7 @@
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
+#include "Tooltip.hpp"
#include "common/DoubleValidator.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/PasswordValidator.hpp"
@@ -51,6 +52,7 @@ namespace lms::ui
{
public:
// Associate each field with a unique string literal.
+ static inline const Field ArtistReleaseSortMethodField{ "artist-release-sort-method" };
static inline const Field TranscodingModeField{ "transcoding-mode" };
static inline const Field TranscodeFormatField{ "transcoding-output-format" };
static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" };
@@ -68,10 +70,11 @@ namespace lms::ui
static inline const Field PasswordField{ "password" };
static inline const Field PasswordConfirmField{ "password-confirm" };
+ using ArtistReleaseSortMethodModel = ValueStringModel;
using TranscodingModeModel = ValueStringModel;
using ReplayGainModeModel = ValueStringModel;
- using FeedbackBackendModel = ValueStringModel;
- using ScrobblingBackendModel = ValueStringModel;
+ using FeedbackBackendModel = ValueStringModel;
+ using ScrobblingBackendModel = ValueStringModel;
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword)
: _authPasswordService{ authPasswordService }
@@ -79,6 +82,7 @@ namespace lms::ui
{
initializeModels();
+ addField(ArtistReleaseSortMethodField);
addField(TranscodingModeField);
addField(TranscodeBitrateField);
addField(TranscodeFormatField);
@@ -106,6 +110,7 @@ namespace lms::ui
addField(PasswordConfirmField);
}
+ setValidator(ArtistReleaseSortMethodField, createMandatoryValidator());
setValidator(TranscodingModeField, createMandatoryValidator());
setValidator(TranscodeBitrateField, createMandatoryValidator());
setValidator(TranscodeFormatField, createMandatoryValidator());
@@ -122,6 +127,7 @@ namespace lms::ui
loadData();
}
+ std::shared_ptr getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
std::shared_ptr getTranscodingModeModel() { return _transcodingModeModeModel; }
std::shared_ptr getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
@@ -136,6 +142,12 @@ namespace lms::ui
User::pointer user{ LmsApp->getUser() };
+ {
+ auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) };
+ if (artistReleaseSortMethodRow)
+ user.modify()->setUIArtistReleaseSortMethod(_artistReleaseSortMethodModel->getValue(*artistReleaseSortMethodRow));
+ }
+
{
MediaPlayer::Settings settings;
@@ -204,6 +216,12 @@ namespace lms::ui
User::pointer user{ LmsApp->getUser() };
+ {
+ auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) };
+ if (artistReleaseSortMethodRow)
+ setValue(ArtistReleaseSortMethodField, _artistReleaseSortMethodModel->getString(*artistReleaseSortMethodRow));
+ }
+
{
const auto settings{ *LmsApp->getMediaPlayer().getSettings() };
@@ -315,6 +333,13 @@ namespace lms::ui
private:
void initializeModels()
{
+ _artistReleaseSortMethodModel = std::make_shared();
+ _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-asc"), db::ReleaseSortMethod::DateAsc);
+ _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-desc"), db::ReleaseSortMethod::DateDesc);
+ _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-asc"), db::ReleaseSortMethod::OriginalDate);
+ _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-desc"), db::ReleaseSortMethod::OriginalDateDesc);
+ _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name);
+
_transcodingModeModeModel = std::make_shared();
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never);
@@ -355,6 +380,7 @@ namespace lms::ui
auth::IPasswordService* _authPasswordService{};
bool _withOldPassword{};
+ std::shared_ptr _artistReleaseSortMethodModel;
std::shared_ptr _transcodingModeModeModel;
std::shared_ptr> _transcodingOutputBitrateModel;
std::shared_ptr> _transcodingOutputFormatModel;
@@ -425,12 +451,25 @@ namespace lms::ui
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
}
+ // User interface
+ {
+ auto artistReleaseSortMethod{ std::make_unique() };
+ artistReleaseSortMethod->setModel(model->getArtistReleaseSortMethodModel());
+ t->setFormWidget(SettingsModel::ArtistReleaseSortMethodField, std::move(artistReleaseSortMethod));
+ }
+
// Audio
{
// Transcode
auto transcodingMode{ std::make_unique() };
- auto* transcodingModeRaw{ transcodingMode.get() };
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
@@ -443,17 +482,15 @@ namespace lms::ui
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
- transcodingModeRaw->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);
+ // Replay gain mode
+ auto replayGainMode{ std::make_unique() };
+ replayGainMode->activated().connect([=](int row) {
+ const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None };
+ model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable);
+ model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
-
- // Replay gain mode
- auto replayGainMode{ std::make_unique() };
- auto* replayGainModeRaw{ replayGainMode.get() };
replayGainMode->setModel(model->getReplayGainModeModel());
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
@@ -467,13 +504,6 @@ namespace lms::ui
replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo));
- replayGainModeRaw->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());
- });
if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None)
{
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
@@ -573,6 +603,8 @@ namespace lms::ui
});
t->updateView(model.get());
+
+ initTooltipsForWidgetTree(*t);
}
} // namespace lms::ui
\ No newline at end of file
diff --git a/src/lms/ui/Tooltip.cpp b/src/lms/ui/Tooltip.cpp
new file mode 100644
index 00000000..efde9dcf
--- /dev/null
+++ b/src/lms/ui/Tooltip.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "Tooltip.hpp"
+
+#include "core/ILogger.hpp"
+#include
+
+namespace lms::ui
+{
+ void initTooltipsForWidgetTree(Wt::WWebWidget& widget)
+ {
+ std::ostringstream oss;
+ oss << R"({const rootElement = document.getElementById(')" << widget.id() << R"(');)"
+ << R"(const tooltipTriggerList = rootElement.querySelectorAll('[data-bs-toggle="tooltip"]');
+tooltipTriggerList.forEach(tooltipTriggerEl => {
+new bootstrap.Tooltip(tooltipTriggerEl);
+});})";
+ LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
+
+ widget.doJavaScript(oss.str());
+ }
+} // namespace lms::ui
\ No newline at end of file
diff --git a/src/lms/ui/Tooltip.hpp b/src/lms/ui/Tooltip.hpp
new file mode 100644
index 00000000..5f216df6
--- /dev/null
+++ b/src/lms/ui/Tooltip.hpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+namespace Wt
+{
+ class WWebWidget;
+}
+
+namespace lms::ui
+{
+ void initTooltipsForWidgetTree(Wt::WWebWidget& widget);
+}
\ No newline at end of file
diff --git a/src/lms/ui/common/ValueStringModel.hpp b/src/lms/ui/common/ValueStringModel.hpp
index c4fded62..e27ccb0b 100644
--- a/src/lms/ui/common/ValueStringModel.hpp
+++ b/src/lms/ui/common/ValueStringModel.hpp
@@ -40,7 +40,7 @@ namespace lms::ui
return Wt::cpp17::any_cast(data(index(static_cast(row), 0), Wt::ItemDataRole::Display));
}
- std::optional getRowFromString(const Wt::WString& value)
+ std::optional getRowFromString(const Wt::WString& value) const
{
for (std::size_t i{}; i < static_cast(rowCount()); ++i)
{
@@ -51,7 +51,7 @@ namespace lms::ui
return std::nullopt;
}
- std::optional getRowFromValue(const T& value)
+ std::optional getRowFromValue(const T& value) const
{
for (std::size_t i{}; i < static_cast(rowCount()); ++i)
{
diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp
index a05b9a9e..aa2b2fed 100644
--- a/src/lms/ui/explore/ArtistView.cpp
+++ b/src/lms/ui/explore/ArtistView.cpp
@@ -197,7 +197,7 @@ namespace lms::ui
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
- params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
+ params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
if (!releases.results.empty())