diff --git a/locales/en/messages.json b/locales/en/messages.json index e5ef3f1b..e29c8eb1 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -6379,9 +6379,13 @@ "message": "Do you really want to close the configurator?" }, "dropDownSelectAll": { - "message": "[Select all]", + "message": "[Select/deselect all]", "description": "Select all item in the drop down/multiple select" }, + "dropDownFilterDisabled": { + "message": "Select...", + "description": "Text indicating nothing is selected in the drop down/multiple select (filter disabled)" + }, "dropDownAll": { "message": "All", "description": "Text indicating everything is selected in the drop down/multiple select" @@ -6577,5 +6581,13 @@ "presetsTooManyPresetsFound": { "message": "Reached the maximum limit of the shown presets number", "description": "Message that apprears on presets tab if too many presets found" + }, + "presetsOptionsPlaceholder": { + "message": "ATTENTION! Review the list of options", + "description": "Placeholder for the options list dropdown" + }, + "presetsVersionMismatch": { + "message": "Preset source version mismatch.
Required version: {{versionRequired}}
Preset source version: {{versionSource}}
Using this preset source could be dangerous.
Do you want to continue?", + "description": "Placeholder for the options list dropdown" } } diff --git a/src/main.html b/src/main.html index 0d1be222..d1a791af 100644 --- a/src/main.html +++ b/src/main.html @@ -137,6 +137,7 @@ + diff --git a/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.css b/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.css index 5a2580a5..a7a3c2d2 100644 --- a/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.css +++ b/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.css @@ -46,6 +46,11 @@ padding-left: 20px; } +#presets_options_panel .ms-choice { + border-color: var(--accentBorder); + border-width: 2px; +} + #presets_detailed_dialog_loading { height: 300px; } diff --git a/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.js b/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.js index b0ec13ea..1cd52d60 100644 --- a/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.js +++ b/src/tabs/presets/DetailedDialog/PresetsDetailedDialog.js @@ -125,24 +125,59 @@ class PresetsDetailedDialog { _createOptionsSelect(options) { options.forEach(option => { - let selectedString = "selected=\"selected\""; - if (!option.checked) { - selectedString = ""; + if (!option.childs) { + this._addOption(this._domOptionsSelect, option, false); + } else { + this._addOptionGroup(this._domOptionsSelect, option); } - - this._domOptionsSelect.append(``); }); this._domOptionsSelect.multipleSelect({ - placeholder: i18n.getMessage("dropDownAll"), + placeholder: i18n.getMessage("presetsOptionsPlaceholder"), formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); }, formatAllSelected() { return i18n.getMessage("dropDownAll"); }, onClick: () => this._optionsSelectionChanged(), onCheckAll: () => this._optionsSelectionChanged(), onUncheckAll: () => this._optionsSelectionChanged(), + hideOptgroupCheckboxes: true, + singleRadio: true, + selectAll: false, + styler: function (row) { + let style = ""; + if (row.type === 'optgroup') { + style = 'font-weight: bold;'; + } else if (row.classes.includes("optionHasParent")) { + style = 'padding-left: 22px;'; + } + return style; + }, }); } + _addOptionGroup(parentElement, optionGroup) { + const optionGroupElement = $(``); + + optionGroup.childs.forEach(option => { + this._addOption(optionGroupElement, option, true); + }); + + parentElement.append(optionGroupElement); + } + + _addOption(parentElement, option, hasParent) { + let selectedString = "selected=\"selected\""; + if (!option.checked) { + selectedString = ""; + } + + let classString = ""; + if (hasParent) { + classString = "class=\"optionHasParent\""; + } + + parentElement.append(``); + } + _optionsSelectionChanged() { this._updateFinalCliText(); } diff --git a/src/tabs/presets/PresetsRepoIndexed/PresetParser.js b/src/tabs/presets/PresetsRepoIndexed/PresetParser.js new file mode 100644 index 00000000..68e6ef9c --- /dev/null +++ b/src/tabs/presets/PresetsRepoIndexed/PresetParser.js @@ -0,0 +1,224 @@ +'use strict'; + +class PresetParser { + constructor(settings) { + this._settings = settings; + } + + readPresetProperties(preset, strings) { + const propertiesToRead = ["description", "discussion", "warning", "disclaimer", "include_warning", "include_disclaimer", "discussion"]; + const propertiesMetadata = {}; + preset.options = []; + + propertiesToRead.forEach(propertyName => { + // metadata of each property, name, type, optional true/false; example: + // keywords: {type: MetadataTypes.WORDS_ARRAY, optional: true} + propertiesMetadata[propertyName] = this._settings.presetsFileMetadata[propertyName]; + preset[propertyName] = undefined; + }); + + preset._currentOptionGroup = undefined; + + for (const line of strings) { + if (line.startsWith(this._settings.MetapropertyDirective)) { + this._parseAttributeLine(preset, line, propertiesMetadata); + } + } + + delete preset._currentOptionGroup; + } + + _parseAttributeLine(preset, line, propertiesMetadata) { + line = line.slice(this._settings.MetapropertyDirective.length).trim(); // (#$ DESCRIPTION: foo) -> (DESCRIPTION: foo) + const lowCaseLine = line.toLowerCase(); + let isProperty = false; + + for (const propertyName in propertiesMetadata) { + const lineBeginning = `${propertyName.toLowerCase()}:`; // "description:" + + if (lowCaseLine.startsWith(lineBeginning)) { + line = line.slice(lineBeginning.length).trim(); // (Title: foo) -> (foo) + this._parseProperty(preset, line, propertyName); + isProperty = true; + } + } + + if (!isProperty && lowCaseLine.startsWith(this._settings.OptionsDirectives.OPTION_DIRECTIVE)) { + this._parseOptionDirective(preset, line); + } + } + + _parseOptionDirective(preset, line) { + const lowCaseLine = line.toLowerCase(); + + if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE)) { + const option = this._getOption(line); + if (!preset._currentOptionGroup) { + preset.options.push(option); + } else { + preset._currentOptionGroup.childs.push(option); + } + } else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE)) { + const optionGroup = this._getOptionGroup(line); + preset._currentOptionGroup = optionGroup; + preset.options.push(optionGroup); + } else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_GROUP_DIRECTIVE)) { + preset._currentOptionGroup = undefined; + } + } + + _getOption(line) { + const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim(); + const directiveRemovedLowCase = directiveRemoved.toLowerCase(); + const OptionChecked = this._isOptionChecked(directiveRemovedLowCase); + + const regExpRemoveChecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_CHECKED), 'gi'); + const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_UNCHECKED), 'gi'); + let optionName = directiveRemoved.replace(regExpRemoveChecked, ""); + optionName = optionName.replace(regExpRemoveUnchecked, "").trim(); + + return { + name: optionName.slice(1).trim(), + checked: OptionChecked, + }; + } + + _getOptionGroup(line) { + const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE.length).trim(); + + return { + name: directiveRemoved.slice(1).trim(), + childs: [], + }; + } + + _isOptionChecked(lowCaseLine) { + return lowCaseLine.includes(this._settings.OptionsDirectives.OPTION_CHECKED); + } + + _parseProperty(preset, line, propertyName) { + switch(this._settings.presetsFileMetadata[propertyName].type) { + case this._settings.MetadataTypes.STRING_ARRAY: + this._processArrayProperty(preset, line, propertyName); + break; + case this._settings.MetadataTypes.STRING: + this._processStringProperty(preset, line, propertyName); + break; + case this._settings.MetadataTypes.FILE_PATH: + this._processStringProperty(preset, line, propertyName); + break; + case this._settings.MetadataTypes.FILE_PATH_ARRAY: + this._processArrayProperty(preset, line, propertyName); + break; + default: + this.console.err(`Parcing preset: unknown property type '${this._settings.presetsFileMetadata[property].type}' for the property '${propertyName}'`); + } + } + + _processArrayProperty(preset, line, propertyName) { + if (!preset[propertyName]) { + preset[propertyName] = []; + } + + preset[propertyName].push(line); + } + + _processStringProperty(preset, line, propertyName) { + preset[propertyName] = line; + } + + _getOptionName(line) { + const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim(); + const regExpRemoveChecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_CHECKED +":"), 'gi'); + const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_UNCHECKED +":"), 'gi'); + let optionName = directiveRemoved.replace(regExpRemoveChecked, ""); + optionName = optionName.replace(regExpRemoveUnchecked, "").trim(); + return optionName; + } + + _escapeRegex(string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + removeUncheckedOptions(strings, checkedOptions) { + let resultStrings = []; + let isCurrentOptionExcluded = false; + const lowerCasedCheckedOptions = checkedOptions.map(optionName => optionName.toLowerCase()); + + strings.forEach(str => { + if (this._isLineAttribute(str)) { + const line = this._removeAttributeDirective(str); + + if (this._isOptionBegin(line)) { + const optionNameLowCase = this._getOptionName(line).toLowerCase(); + + if (!lowerCasedCheckedOptions.includes(optionNameLowCase)) { + isCurrentOptionExcluded = true; + } + } else if (this._isOptionEnd(line)) { + isCurrentOptionExcluded = false; + } + } else if (!isCurrentOptionExcluded) { + resultStrings.push(str); + } + }); + + resultStrings = this._removeExcessiveEmptyLines(resultStrings); + + return resultStrings; + } + + _removeExcessiveEmptyLines(strings) { + // removes empty lines if there are two or more in a row leaving just one empty line + const result = []; + let lastStringEmpty = false; + + strings.forEach(str => { + if ("" !== str || !lastStringEmpty) { + result.push(str); + } + + if ("" === str) { + lastStringEmpty = true; + } else { + lastStringEmpty = false; + } + }); + + return result; + } + + _isLineAttribute(line) { + return line.trim().startsWith(this._settings.MetapropertyDirective); + } + + _isOptionBegin(line) { + const lowCaseLine = line.toLowerCase(); + return lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE); + } + + _isOptionEnd(line) { + const lowCaseLine = line.toLowerCase(); + return lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_DIRECTIVE); + } + + _removeAttributeDirective(line) { + return line.trim().slice(this._settings.MetapropertyDirective.length).trim(); + } + + isIncludeFound(strings) { + for (const str of strings) { + const match = PresetParser._sRegExpInclude.exec(str); + + if (match !== null) { + return true; + } + } + + return false; + } + +} + +// Reg exp extracts file/path.txt from # include: file/path.txt +PresetParser._sRegExpInclude = /^#\$[ ]+?INCLUDE:[ ]+?(?\S+$)/; diff --git a/src/tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js b/src/tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js index 75fecd04..51397a1b 100644 --- a/src/tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js +++ b/src/tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js @@ -14,96 +14,25 @@ class PresetsRepoIndexed { loadIndex() { return fetch(this._urlRaw + "index.json", {cache: "no-cache"}) .then(res => res.json()) - .then(out => this._index = out); - } - - removeUncheckedOptions(strings, checkedOptions) { - let resultStrings = []; - let isCurrentOptionExcluded = false; - const lowerCasedCheckedOptions = checkedOptions.map(optionName => optionName.toLowerCase()); - - strings.forEach(str => { - if (this._isLineAttribute(str)) { - const line = this._removeAttributeDirective(str); - - if (this._isOptionBegin(line)) { - const optionNameLowCase = this._getOptionName(line).toLowerCase(); - - if (!lowerCasedCheckedOptions.includes(optionNameLowCase)) { - isCurrentOptionExcluded = true; - } - } else if (this._isOptionEnd(line)) { - isCurrentOptionExcluded = false; - } - } else if (!isCurrentOptionExcluded) { - resultStrings.push(str); - } - }); - - resultStrings = this._removeExcessiveEmptyLines(resultStrings); - - return resultStrings; - } - - _removeExcessiveEmptyLines(strings) { - // removes empty lines if there are two or more in a row leaving just one empty line - const result = []; - let lastStringEmpty = false; - - strings.forEach(str => { - if ("" !== str || !lastStringEmpty) { - result.push(str); - } - - if ("" === str) { - lastStringEmpty = true; - } else { - lastStringEmpty = false; - } - }); - - return result; - } - - _isLineAttribute(line) { - return line.trim().startsWith(PresetsRepoIndexed._sCliAttributeDirective); - } - - _isOptionBegin(line) { - const lowCaseLine = line.toLowerCase(); - return lowCaseLine.startsWith(this._index.settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE); - } - - _isOptionEnd(line) { - const lowCaseLine = line.toLowerCase(); - return lowCaseLine.startsWith(this._index.settings.OptionsDirectives.END_OPTION_DIRECTIVE); - } - - _getOptionName(line) { - const directiveRemoved = line.slice(this._index.settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim(); - const regExpRemoveChecked = new RegExp(this._escapeRegex(this._index.settings.OptionsDirectives.OPTION_CHECKED +":"), 'gi'); - const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._index.settings.OptionsDirectives.OPTION_UNCHECKED +":"), 'gi'); - let optionName = directiveRemoved.replace(regExpRemoveChecked, ""); - optionName = optionName.replace(regExpRemoveUnchecked, "").trim(); - return optionName; - } - - _escapeRegex(string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - - _removeAttributeDirective(line) { - return line.trim().slice(PresetsRepoIndexed._sCliAttributeDirective.length).trim(); + .then(out => { + this._index = out; + this._settings = this._index.settings; + this._PresetParser = new PresetParser(this._index.settings); + }); } getPresetOnlineLink(preset) { return this._urlViewOnline + preset.fullPath; } + removeUncheckedOptions(strings, checkedOptions) { + return this._PresetParser.removeUncheckedOptions(strings, checkedOptions); + } + _parceInclude(strings, includeRowIndexes, promises) { for (let i = 0; i < strings.length; i++) { - const match = PresetsRepoIndexed._sRegExpInclude.exec(strings[i]); + const match = PresetParser._sRegExpInclude.exec(strings[i]); if (match !== null) { includeRowIndexes.push(i); @@ -131,7 +60,7 @@ class PresetsRepoIndexed { } _executeIncludeNested(strings) { - const isIncludeFound = this._isIncludeFound(strings); + const isIncludeFound = this._PresetParser.isIncludeFound(strings); if (isIncludeFound) { return this._executeIncludeOnce(strings) @@ -141,18 +70,6 @@ class PresetsRepoIndexed { } } - _isIncludeFound(strings) { - for (const str of strings) { - const match = PresetsRepoIndexed._sRegExpInclude.exec(str); - - if (match !== null) { - return true; - } - } - - return false; - } - loadPreset(preset) { const promiseMainText = this._loadPresetText(this._urlRaw + preset.fullPath); @@ -160,6 +77,7 @@ class PresetsRepoIndexed { .then(text => { let strings = text.split("\n"); strings = strings.map(str => str.trim()); + this._PresetParser.readPresetProperties(preset, strings); return strings; }) .then(strings => this._executeIncludeNested(strings)) @@ -215,9 +133,3 @@ class PresetsRepoIndexed { }); } } - -PresetsRepoIndexed._sCliCommentDirective = "#"; -PresetsRepoIndexed._sCliAttributeDirective = "#$"; - -// Reg exp extracts file/path.txt from # include: file/path.txt -PresetsRepoIndexed._sRegExpInclude = /^#\$[ ]+?INCLUDE:[ ]+?(?\S+$)/; diff --git a/src/tabs/presets/presets.css b/src/tabs/presets/presets.css index 1ceacb8c..af712dfd 100644 --- a/src/tabs/presets/presets.css +++ b/src/tabs/presets/presets.css @@ -287,11 +287,25 @@ } /* hack-fix for multiple-select with small font size. Originally checkboxes are not ligned with the text options when the font size is smaller than 0.7em */ -.tab-presets -.ms-drop input[type="radio"], .ms-drop input[type="checkbox"] { +.tab-presets .ms-drop input[type="radio"], .ms-drop input[type="checkbox"] { margin-top: 0.1rem !important; } +/* hack-fix for multiple-select. Places "X" button vertically in the middle for the current font. "X" icon clears the current selection. */ +.tab-presets .ms-choice>div.icon-close { + padding-top: 3px; +} + +/* hack-fix for multiple-select. Prevents "X" button from changing color on hover. "X" icon clears the current selection */ +.tab-presets .ms-choice > div.icon-close:hover::before { + color: rgb(136, 136, 136); +} + +/* hack-fix for multiple-select. Makes "X" button bigger. "X" icon clears the current selection */ +.tab-presets .ms-choice>div.icon-close { + font-size: 16px; +} + .presets_filter_table_wrapper { display: grid; grid-gap: 5px; diff --git a/src/tabs/presets/presets.js b/src/tabs/presets/presets.js index d9ad3a3e..6696f26e 100644 --- a/src/tabs/presets/presets.js +++ b/src/tabs/presets/presets.js @@ -4,6 +4,7 @@ TABS.presets = { presetsRepo: null, cliEngine: null, pickedPresetList: [], + majorVersion: 1, }; TABS.presets.initialize = function (callback) { @@ -292,7 +293,9 @@ TABS.presets.tryLoadPresets = function() { this._divGlobalLoading.toggle(true); this._domWarningNotOfficialSource.toggle(!this.presetsSourcesDialog.isOfficialActive); - this.presetsRepo.loadIndex().then(() => { + this.presetsRepo.loadIndex() + .then(() => this.checkPresetSourceVersion()) + .then(() => { this.prepareFilterFields(); this._divGlobalLoading.toggle(false); this._divMainContent.toggle(true); @@ -303,13 +306,35 @@ TABS.presets.tryLoadPresets = function() { }); }; +TABS.presets.checkPresetSourceVersion = function() { + return new Promise((resolve, reject) => { + if (this.majorVersion === this.presetsRepo.index.majorVersion) { + resolve(); + } else { + const versionRequired = `${this.majorVersion}.X`; + const versionSource = `${this.presetsRepo.index.majorVersion}.${this.presetsRepo.index.minorVersion}`; + + const dialogSettings = { + title: i18n.getMessage("presetsWarningDialogTitle"), + text: i18n.getMessage("presetsVersionMismatch", {"versionRequired": versionRequired, "versionSource":versionSource}), + buttonYesText: i18n.getMessage("yes"), + buttonNoText: i18n.getMessage("no"), + buttonYesCallback: () => resolve(), + buttonNoCallback: () => reject("Prset source version mismatch"), + }; + + GUI.showYesNoDialog(dialogSettings); + } + }); +}; + TABS.presets.prepareFilterFields = function() { this._freezeSearch = true; - this.prepareFilterSelectField(this._selectCategory, this.presetsRepo.index.uniqueValues.category); - this.prepareFilterSelectField(this._selectKeyword, this.presetsRepo.index.uniqueValues.keywords); - this.prepareFilterSelectField(this._selectAuthor, this.presetsRepo.index.uniqueValues.author); - this.prepareFilterSelectField(this._selectFirmwareVersion, this.presetsRepo.index.uniqueValues.firmware_version); - this.prepareFilterSelectField(this._selectStatus, this.presetsRepo.index.settings.PresetStatusEnum); + this.prepareFilterSelectField(this._selectCategory, this.presetsRepo.index.uniqueValues.category, 3); + this.prepareFilterSelectField(this._selectKeyword, this.presetsRepo.index.uniqueValues.keywords, 3); + this.prepareFilterSelectField(this._selectAuthor, this.presetsRepo.index.uniqueValues.author, 1); + this.prepareFilterSelectField(this._selectFirmwareVersion, this.presetsRepo.index.uniqueValues.firmware_version, 2); + this.prepareFilterSelectField(this._selectStatus, this.presetsRepo.index.settings.PresetStatusEnum, 2); this.preselectFilterFields(); this._inputTextFilter.on('input', () => this.updateSearchResults()); @@ -320,8 +345,6 @@ TABS.presets.prepareFilterFields = function() { }; TABS.presets.preselectFilterFields = function() { - this._selectCategory.multipleSelect('setSelects', ["TUNE", "RC_SMOOTHING", "RC_LINK", "RATES"]); - const currentVersion = FC.CONFIG.flightControllerVersion; const selectedVersions = []; @@ -334,15 +357,17 @@ TABS.presets.preselectFilterFields = function() { this._selectFirmwareVersion.multipleSelect('setSelects', selectedVersions); }; -TABS.presets.prepareFilterSelectField = function(domSelectElement, selectOptions) { +TABS.presets.prepareFilterSelectField = function(domSelectElement, selectOptions, minimumCountSelected) { domSelectElement.multipleSelect("destroy"); domSelectElement.multipleSelect({ data: selectOptions, - placeholder: i18n.getMessage("dropDownAll"), + showClear: true, + minimumCountSelected : minimumCountSelected, + placeholder: i18n.getMessage("dropDownFilterDisabled"), onClick: () => { this.updateSearchResults(); }, onCheckAll: () => { this.updateSearchResults(); }, onUncheckAll: () => { this.updateSearchResults(); }, - formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); }, + formatSelectAll() { return i18n.getMessage("dropDownSelectAll"); }, formatAllSelected() { return i18n.getMessage("dropDownAll"); }, }); }; @@ -411,6 +436,8 @@ TABS.presets.getFitPresets = function(searchParams) { } } + result.sort((a, b) => (a.priority > b.priority) ? -1 : 1); + return result; }; @@ -478,10 +505,10 @@ TABS.presets.isPresetFitSearchFirmwareVersions = function(preset, searchParams) TABS.presets.isPresetFitSearchString = function(preset, searchParams) { - if (searchParams.searchString) - { + if (searchParams.searchString) { const allKeywords = preset.keywords.join(" "); - const totalLine = [preset.description, allKeywords, preset.title, preset.author].join("\n").toLowerCase().replace("''", "\""); + const allVersions = preset.firmware_version.join(" "); + const totalLine = [preset.description, allKeywords, preset.title, preset.author, allVersions, preset.category].join("\n").toLowerCase().replace("''", "\""); const allWords = searchParams.searchString.toLowerCase().replace("''", "\"").split(" "); for (const word of allWords) {