Added an option to select how to sort albums in the artist view, fixes #498
This commit is contained in:
@@ -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{};
|
||||
|
||||
@@ -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<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<UIState>> _uiStates;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -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
|
||||
|
||||
+50
-18
@@ -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<db::ReleaseSortMethod>;
|
||||
using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcoding::Mode>;
|
||||
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
|
||||
using FeedbackBackendModel = ValueStringModel<FeedbackBackend>;
|
||||
using ScrobblingBackendModel = ValueStringModel<ScrobblingBackend>;
|
||||
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
|
||||
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
|
||||
|
||||
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<ArtistReleaseSortMethodModel> getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
|
||||
std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> 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>();
|
||||
_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<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);
|
||||
@@ -355,6 +380,7 @@ namespace lms::ui
|
||||
auth::IPasswordService* _authPasswordService{};
|
||||
bool _withOldPassword{};
|
||||
|
||||
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
|
||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
||||
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
||||
@@ -425,12 +451,25 @@ namespace lms::ui
|
||||
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));
|
||||
}
|
||||
|
||||
// Audio
|
||||
{
|
||||
// Transcode
|
||||
auto transcodingMode{ std::make_unique<Wt::WComboBox>() };
|
||||
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<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());
|
||||
});
|
||||
|
||||
// Replay gain mode
|
||||
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
|
||||
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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Tooltip.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include <Wt/WWebWidget.h>
|
||||
|
||||
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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Wt
|
||||
{
|
||||
class WWebWidget;
|
||||
}
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
void initTooltipsForWidgetTree(Wt::WWebWidget& widget);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace lms::ui
|
||||
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
||||
}
|
||||
|
||||
std::optional<std::size_t> getRowFromString(const Wt::WString& value)
|
||||
std::optional<std::size_t> getRowFromString(const Wt::WString& value) const
|
||||
{
|
||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||
{
|
||||
@@ -51,7 +51,7 @@ namespace lms::ui
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> getRowFromValue(const T& value)
|
||||
std::optional<std::size_t> getRowFromValue(const T& value) const
|
||||
{
|
||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user