diff --git a/approot/admin-scansettings.xml b/approot/admin-scansettings.xml index 0e760a7a..17c244af 100644 --- a/approot/admin-scansettings.xml +++ b/approot/admin-scansettings.xml @@ -36,35 +36,39 @@
- ${extra-tags-to-scan class="form-control"} -
- ${extra-tags-to-scan-info} -
+ ${extra-tags-to-scan-container class="row gy-3"}
-
-
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
-
+ + + +
+ ${value class="form-control"} + ${del-btn class="btn btn-sm btn-outline-warning"} +
+ ${value-info} +
+
+
diff --git a/approot/messages.xml b/approot/messages.xml index 9ba2dc91..4f1d5872 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -65,10 +65,10 @@ Root directory -Delimiter to be used for splitting artist tags +Delimiters to be used for splitting artist tags Daily -Delimiter to be used for splitting other tags -Extra tags to scan (use ; as separator) +Delimiters to be used for splitting other tags +Extra tags to scan Hourly Scan now! Monthly diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 54b33a6f..6a61b253 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -65,10 +65,10 @@ Répertoire racine -Délimiteur à utiliser pour séparer les tags d'artistes +Délimiteurs à utiliser pour séparer les tags d'artistes Tous les jours -Délimiteur à utiliser pour séparer les autres tags -Tags supplémentaires à scanner (utiliser ; comme séparateur) +Délimiteurs à utiliser pour séparer les autres tags +Tags supplémentaires à scanner Toutes les heures Scanner maintenant ! Tous les mois diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 409b048c..631454f9 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -65,10 +65,10 @@ Cartella principale -Delimitatore da utilizzare per separare i tag degli artisti +Delimitatori da utilizzare per separare i tag degli artisti Giornaliera -Delimitatore da utilizzare per separare gli altri tag -Tag aggiuntivi da scansionare (usa ; come separatore) +Delimitatori da utilizzare per separare gli altri tag +Tag aggiuntivi da scansionare Ogni ora Scansiona ora! Mensile diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 2707e0b3..b7e0a884 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -66,10 +66,10 @@ Katalog główny -Znak rozdzielający artystów +Znaki rozdzielające artystów Codziennie -Znak rozdzielający inne oznaczenia -Szukaj dodatkowych znaczników (użyj ; do rozdzielania) +Znaki rozdzielające inne oznaczenia +Szukaj dodatkowych znaczników Co godzinę Skanuj teraz! Co miesiąc diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp index 5196f82c..0bebc269 100644 --- a/src/libs/database/impl/ScanSettings.cpp +++ b/src/libs/database/impl/ScanSettings.cpp @@ -72,7 +72,7 @@ namespace lms::db return core::stringUtils::splitEscapedStrings(_defaultTagDelimiters, ';', '\\'); } - void ScanSettings::setExtraTagsToScan(const std::vector& extraTags) + void ScanSettings::setExtraTagsToScan(std::span extraTags) { std::string newTagsToScan{ core::stringUtils::joinStrings(extraTags, ";") }; if (newTagsToScan != _extraTagsToScan) diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp index 91ffd438..95ac28d0 100644 --- a/src/libs/database/include/database/ScanSettings.hpp +++ b/src/libs/database/include/database/ScanSettings.hpp @@ -75,7 +75,7 @@ namespace lms::db // Setters void setUpdateStartTime(Wt::WTime t) { _startTime = t; } void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; } - void setExtraTagsToScan(const std::vector& extraTags); + void setExtraTagsToScan(std::span extraTags); void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; } void setArtistTagDelimiters(std::span delimiters); void setDefaultTagDelimiters(std::span delimiters); diff --git a/src/lms/ui/admin/ScanSettingsView.cpp b/src/lms/ui/admin/ScanSettingsView.cpp index 6621aa57..872bd2c7 100644 --- a/src/lms/ui/admin/ScanSettingsView.cpp +++ b/src/lms/ui/admin/ScanSettingsView.cpp @@ -51,7 +51,7 @@ namespace lms::ui Wt::WValidator::Result validate(const Wt::WString& input) const override { if (input.empty()) - return Wt::WValidator::Result{ Wt::ValidationState::Valid }; + return Wt::WValidator::validate(input); std::string inputStr{ input.toUTF8() }; if (std::all_of(std::cbegin(inputStr), std::cend(inputStr), [](char c) { return std::isspace(c); })) @@ -69,14 +69,9 @@ namespace lms::ui static inline constexpr Field UpdatePeriodField{ "update-period" }; static inline constexpr Field UpdateStartTimeField{ "update-start-time" }; static inline constexpr Field SimilarityEngineTypeField{ "similarity-engine-type" }; - static inline constexpr Field ExtraTagsField{ "extra-tags-to-scan" }; - static inline constexpr Field ArtistTagDelimiterField{ "artist-tag-delimiter" }; - static inline constexpr Field DefaultTagDelimiterField{ "default-tag-delimiter" }; using UpdatePeriodModel = ValueStringModel; - static inline constexpr char extraTagsDelimiter{ ';' }; - DatabaseSettingsModel() { initializeModels(); @@ -84,26 +79,17 @@ namespace lms::ui addField(UpdatePeriodField); addField(UpdateStartTimeField); addField(SimilarityEngineTypeField); - addField(ExtraTagsField); - addField(ArtistTagDelimiterField); - addField(DefaultTagDelimiterField); setValidator(UpdatePeriodField, createMandatoryValidator()); setValidator(UpdateStartTimeField, createMandatoryValidator()); setValidator(SimilarityEngineTypeField, createMandatoryValidator()); - setValidator(ExtraTagsField, createUppercaseValidator()); - setValidator(ArtistTagDelimiterField, std::make_unique()); - setValidator(DefaultTagDelimiterField, std::make_unique()); - - // populate the model with initial data - loadData(); } std::shared_ptr updatePeriodModel() { return _updatePeriodModel; } std::shared_ptr updateStartTimeModel() { return _updateStartTimeModel; } std::shared_ptr similarityEngineTypeModel() { return _similarityEngineTypeModel; } - void loadData() + void loadData(std::vector& extraTagsToScan, std::vector& artistDelimiters, std::vector& defaultDelimiters) { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -128,20 +114,13 @@ namespace lms::ui setValue(SimilarityEngineTypeField, _similarityEngineTypeModel->getString(*similarityEngineTypeRow)); const auto extraTags{ scanSettings->getExtraTagsToScan() }; - setValue(ExtraTagsField, core::stringUtils::joinStrings(extraTags, extraTagsDelimiter)); - - { - std::vector delimiters{ scanSettings->getArtistTagDelimiters() }; - setValue(ArtistTagDelimiterField, delimiters.empty() ? "" : delimiters.front()); - } - - { - std::vector delimiters{ scanSettings->getDefaultTagDelimiters() }; - setValue(DefaultTagDelimiterField, delimiters.empty() ? "" : delimiters.front()); - } + extraTagsToScan.clear(); + std::transform(std::cbegin(extraTags), std::cend(extraTags), std::back_inserter(extraTagsToScan), [](std::string_view extraTag) { return std::string{ extraTag }; }); + artistDelimiters = scanSettings->getArtistTagDelimiters(); + defaultDelimiters = scanSettings->getDefaultTagDelimiters(); } - void saveData() + void saveData(std::span extraTagsToScan, std::span artistDelimiters, std::span defaultDelimiters) { auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; @@ -159,21 +138,9 @@ namespace lms::ui if (similarityEngineTypeRow) scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow)); - scanSettings.modify()->setExtraTagsToScan(core::stringUtils::splitString(valueText(ExtraTagsField).toUTF8(), extraTagsDelimiter)); - - { - std::vector artistDelimiters; - if (std::string artistDelimiter{ valueText(ArtistTagDelimiterField).toUTF8() }; !artistDelimiter.empty()) - artistDelimiters.push_back(std::move(artistDelimiter)); - scanSettings.modify()->setArtistTagDelimiters(artistDelimiters); - } - - { - std::vector defaultDelimiters; - if (std::string defaultDelimiter{ valueText(DefaultTagDelimiterField).toUTF8() }; !defaultDelimiter.empty()) - defaultDelimiters.push_back(std::move(defaultDelimiter)); - scanSettings.modify()->setDefaultTagDelimiters(defaultDelimiters); - } + scanSettings.modify()->setExtraTagsToScan(extraTagsToScan); + scanSettings.modify()->setArtistTagDelimiters(artistDelimiters); + scanSettings.modify()->setDefaultTagDelimiters(defaultDelimiters); } private: @@ -202,6 +169,118 @@ namespace lms::ui std::shared_ptr> _updateStartTimeModel; std::shared_ptr> _similarityEngineTypeModel; }; + + class LineEditEntryModel : public Wt::WFormModel + { + public: + static inline constexpr Field ValueField{ "value" }; + + LineEditEntryModel(const Wt::WString& initialValue, std::shared_ptr validator) + : Wt::WFormModel() + { + addField(ValueField); + + setValidator(ValueField, validator); + setValue(ValueField, initialValue); + } + }; + + class LineEditEntryWidget : public Wt::WTemplateFormView + { + public: + LineEditEntryWidget(const Wt::WString& initialValue, std::shared_ptr validator) + : Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.Database.template.line-edit-entry") } + , _model{ std::make_shared(initialValue, validator) } + { + setStyleClass("col-sm-4 col-md-3"); // hack + + setFormWidget(LineEditEntryModel::ValueField, std::make_unique()); + + auto* delBtn{ bindNew("del-btn", Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) }; + delBtn->clicked().connect(this, [this] { deleted.emit(); }); + } + + bool validate() { return _model->validate(); } + void updateModel() { Wt::WTemplateFormView::updateModel(_model.get()); } + void updateView() { Wt::WTemplateFormView::updateView(_model.get()); } + + Wt::WString getValue() const + { + return _model->valueText(LineEditEntryModel::ValueField); + } + + Wt::Signal<> deleted; + std::shared_ptr _model; + }; + + // Terrible hack to make use of the validation system for each added element + class LineEditContainerWidget : public Wt::WContainerWidget + { + public: + LineEditContainerWidget(std::shared_ptr validator) + : _validator{ validator } {} + + void add(const Wt::WString& value = "") + { + auto* entry{ addNew(value, _validator) }; + + entry->deleted.connect(this, [=, this] { + removeWidget(entry); + }); + } + + bool validate() + { + bool res{ true }; + + for (int i{}; i < count(); ++i) + { + LineEditEntryWidget* entry{ static_cast(widget(i)) }; + res &= entry->validate(); + } + + return res; + } + + void updateModels() + { + for (int i{}; i < count(); ++i) + { + LineEditEntryWidget* entry{ static_cast(widget(i)) }; + entry->updateModel(); + } + } + + void updateViews() + { + for (int i{}; i < count(); ++i) + { + LineEditEntryWidget* entry{ static_cast(widget(i)) }; + entry->updateView(); + } + } + + void visitValues(std::function visitor) const + { + for (int i{}; i < count(); ++i) + { + LineEditEntryWidget* entry{ static_cast(widget(i)) }; + visitor(entry->getValue()); + } + } + + std::vector getValues() const + { + std::vector values; + visitValues([&](const Wt::WString& value) { + values.push_back(value.toUTF8()); + }); + return values; + } + + private: + std::shared_ptr _validator; + }; } // namespace ScanSettingsView::ScanSettingsView() @@ -245,24 +324,102 @@ namespace lms::ui t->setFormWidget(DatabaseSettingsModel::SimilarityEngineTypeField, std::move(similarityEngineType)); // Extra tags - t->setFormWidget(DatabaseSettingsModel::ExtraTagsField, std::make_unique()); + std::shared_ptr extraTagValidator{ createUppercaseValidator() }; + extraTagValidator->setMandatory(true); + auto* extraTagsToScan{ t->bindNew("extra-tags-to-scan-container", extraTagValidator) }; + { + auto* addExtraScanToScanBtn{ t->bindNew("extra-tags-to-scan-add-btn", Wt::WString::tr("Lms.add")) }; + addExtraScanToScanBtn->clicked().connect(this, [=] { + extraTagsToScan->add(); + }); + } // Artist tag delimiter - t->setFormWidget(DatabaseSettingsModel::ArtistTagDelimiterField, std::make_unique()); + std::shared_ptr tagDelimiterValidator{ std::make_shared() }; + tagDelimiterValidator->setMandatory(true); + + auto* artistTagDelimiters{ t->bindNew("artist-tag-delimiter-container", tagDelimiterValidator) }; + { + auto* addArtistDelimiterBtn{ t->bindNew("artist-tag-delimiter-add-btn", Wt::WString::tr("Lms.add")) }; + addArtistDelimiterBtn->clicked().connect(this, [=] { + artistTagDelimiters->add(); + }); + } // Default tag delimiter - t->setFormWidget(DatabaseSettingsModel::DefaultTagDelimiterField, std::make_unique()); + auto* defaultTagDelimiters{ t->bindNew("default-tag-delimiter-container", tagDelimiterValidator) }; + { + auto* addDefaultDelimiterBtn{ t->bindNew("default-tag-delimiter-add-btn", Wt::WString::tr("Lms.add")) }; + addDefaultDelimiterBtn->clicked().connect(this, [=] { + defaultTagDelimiters->add(); + }); + } // Buttons - Wt::WPushButton* saveBtn = t->bindWidget("save-btn", std::make_unique(Wt::WString::tr("Lms.save"))); - Wt::WPushButton* discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))); + Wt::WPushButton* saveBtn{ t->bindWidget("save-btn", std::make_unique(Wt::WString::tr("Lms.save"))) }; + Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))) }; + + auto validate{ [=] { + bool res{ true }; + + res &= model->validate(); + res &= extraTagsToScan->validate(); + res &= artistTagDelimiters->validate(); + res &= defaultTagDelimiters->validate(); + + return res; + } }; + + auto updateModels{ [=] { + t->updateModel(model.get()); + extraTagsToScan->updateModels(); + artistTagDelimiters->updateModels(); + defaultTagDelimiters->updateModels(); + } }; + + auto updateViews{ [=] { + t->updateView(model.get()); + extraTagsToScan->updateViews(); + artistTagDelimiters->updateViews(); + defaultTagDelimiters->updateViews(); + } }; + + auto loadInitialData{ [=] { + std::vector extraTags; + std::vector artistDelimiters; + std::vector defaultDelimiters; + model->loadData(extraTags, artistDelimiters, defaultDelimiters); + + extraTagsToScan->clear(); + for (const std::string& extraTag : extraTags) + extraTagsToScan->add(Wt::WString::fromUTF8(std::string{ extraTag })); + + artistTagDelimiters->clear(); + for (const std::string& artistDelimiter : artistDelimiters) + artistTagDelimiters->add(Wt::WString::fromUTF8(artistDelimiter)); + + defaultTagDelimiters->clear(); + for (const std::string& defaultDelimiter : defaultDelimiters) + defaultTagDelimiters->add(Wt::WString::fromUTF8(defaultDelimiter)); + } }; saveBtn->clicked().connect([=] { - t->updateModel(model.get()); - - if (model->validate()) + updateModels(); + if (validate()) { - model->saveData(); + const std::vector extraTags{ extraTagsToScan->getValues() }; + std::vector extraTagViews; + std::transform(std::cbegin(extraTags), std::cend(extraTags), std::back_inserter(extraTagViews), [](const std::string& tag) -> std::string_view { return tag; }); + + const std::vector artistDelimiters{ artistTagDelimiters->getValues() }; + std::vector artistDelimiterViews; + std::transform(std::cbegin(artistDelimiters), std::cend(artistDelimiters), std::back_inserter(artistDelimiterViews), [](const std::string& delimiter) -> std::string_view { return delimiter; }); + + const std::vector defaultDelimiters{ defaultTagDelimiters->getValues() }; + std::vector defaultDelimiterViews; + std::transform(std::cbegin(defaultDelimiters), std::cend(defaultDelimiters), std::back_inserter(defaultDelimiterViews), [](const std::string& delimiter) -> std::string_view { return delimiter; }); + + model->saveData(extraTagViews, artistDelimiterViews, defaultDelimiterViews); core::Service::get()->load(); // Don't want the scanner to go on with wrong settings @@ -271,16 +428,16 @@ namespace lms::ui } // Udate the view: Delete any validation message in the view, etc. - t->updateView(model.get()); + updateViews(); }); discardBtn->clicked().connect([=] { - model->loadData(); - model->validate(); - t->updateView(model.get()); + loadInitialData(); + validate(); + updateViews(); }); - t->updateView(model.get()); + loadInitialData(); + updateViews(); } - } // namespace lms::ui