1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-16 21:05:30 +03:00

Presets TAB

This commit is contained in:
Ivan Efimov 2021-11-23 19:38:48 -06:00
parent 41747c65b6
commit e63a5fe642
39 changed files with 3375 additions and 19 deletions

View file

@ -31,6 +31,9 @@
"noticeTitle": { "noticeTitle": {
"message": "Notice" "message": "Notice"
}, },
"dontShowAgain": {
"message": "Don't show again"
},
"operationNotSupported": { "operationNotSupported": {
"message": "This operation is not supported by your hardware." "message": "This operation is not supported by your hardware."
}, },
@ -68,6 +71,9 @@
"close": { "close": {
"message": "Close" "message": "Close"
}, },
"OK": {
"message": "OK"
},
"cancel": { "cancel": {
"message": "Cancel" "message": "Cancel"
}, },
@ -86,6 +92,9 @@
"permanentExpertMode": { "permanentExpertMode": {
"message": "Permanently enable Expert Mode" "message": "Permanently enable Expert Mode"
}, },
"warningSettings": {
"message": "Show warnings"
},
"rememberLastTab": { "rememberLastTab": {
"message": "Reopen last tab on connect" "message": "Reopen last tab on connect"
}, },
@ -6205,5 +6214,205 @@
}, },
"cordovaExitAppMessage": { "cordovaExitAppMessage": {
"message": "Do you really want to close the configurator?" "message": "Do you really want to close the configurator?"
},
"dropDownSelectAll": {
"message": "[Select all]",
"description": "Select all item in the drop down/multiple select"
},
"dropDownAll": {
"message": "All",
"description": "Text indicating everything is selected in the drop down/multiple select"
},
"tabPresets": {
"message": "Presets",
"description": "Presets tab title"
},
"presetsReload": {
"message": "Reload",
"description": "Text on the reload button that appears when there in an error loading presets index"
},
"presetsAuthor": {
"message": "Author:",
"description": "Hint text in the presets detailed dialog"
},
"presetsKeywords": {
"message": "Keywords:",
"description": "Hint text in the presets detailed dialog"
},
"presetsVersions": {
"message": "Firmware:",
"description": "Hint text in the presets detailed dialog"
},
"presetsOfficial": {
"message": "Official",
"description": "Hint text in the presets detailed dialog indication preset is official"
},
"presetsCommunity": {
"message": "Community",
"description": "Hint text in the presets detailed dialog indication preset is not official but community"
},
"presetsExperimental": {
"message": "Experimental",
"description": "Hint text in the presets detailed dialog indication preset is not official but experimental"
},
"presetsApply": {
"message": "Pick",
"description": "Button to pick a preset"
},
"presetsViewOnline": {
"message": "View online…",
"description": "Link text for opening preset file online"
},
"presetsOpenDiscussion": {
"message": "Discussion…",
"description": "Link text for opening preset discussion"
},
"presetsShowCli": {
"message": "Show CLI",
"description": "Button to show CLI code of a preset"
},
"presetsHideCli": {
"message": "Hide CLI",
"description": "Button to hide CLI code of a preset"
},
"presetsOptions": {
"message": "Options",
"description": "Text label for Options drop down select"
},
"presetsFilterCategory": {
"message": "Categories",
"description": "UI filter name"
},
"presetsFilterKeyword": {
"message": "Keywords",
"description": "UI filter name"
},
"presetsFilterAuthor": {
"message": "Authors",
"description": "UI filter name"
},
"presetsFilterFirmware": {
"message": "Firmwares",
"description": "UI filter name"
},
"presetsFilterStatus": {
"message": "Status",
"description": "UI filter name - official/community/experimental"
},
"presetsLoadError": {
"message": "Error loading presets from the internet",
"description": "Error report when failed to load presets index or a specific preset"
},
"presetsButtonSave": {
"message": "Save and Reboot",
"description": "A button that saves all appied presets - analog to 'save' command in CLI"
},
"presetsButtonCancel": {
"message": "Cancel",
"description": "A button that restarts FC without saving appied presets - analog to 'exit' command in CLI"
},
"presetsApplyingPresets": {
"message": "Applying configuration...",
"description": "First label in the progress dialog when applying configuration (presets or user config)"
},
"presetsPleaseWait": {
"message": "Please wait.",
"description": "Second label in the progress dialog when applying presets"
},
"presetsCliErrorsWaring": {
"message": "<span class='message-negative'>WARNING!</span><br/>Configuration has been applied with CLI errors.",
"description": "Text to show when there are CLI errors after applying presets or user configuration"
},
"presetsSaveAnyway": {
"message": "Save anyway",
"description": "Save anyway button on the CLI errors presets dialog"
},
"presetsWarningDialogTitle": {
"message": "WARNING!",
"description": "Warning title in the warning dialog in the presets"
},
"presetsWarningDialogYesButton": {
"message": "Agree",
"description": "Agree button in the presets warning dialog"
},
"presetsWarningDialogNoButton": {
"message": "Cancel",
"description": "Cancel button in the presets warning dialog"
},
"presetsWiki": {
"message": "Presets Wiki",
"description": "Button to open Presets Wiki link"
},
"presetsBackupSave": {
"message": "Save backup",
"description": "Button to backup current configuration to file"
},
"presetsBackupLoad": {
"message": "Load backup",
"description": "Button to load backup from the file"
},
"presetsLoadingDumpAll": {
"message": "Loading current configuration from the flight controller",
"description": "Title for the waiting dialog when loading dump all into a file"
},
"dumpAllNotSavedWarning": {
"message": "Error occured while saving current configuration",
"description": "Message appears on presets tab when saving current diff all into a file has failed"
},
"presetSources": {
"message": "Preset sources...",
"description": "A button to show preset sources dialog"
},
"presetsSourcesDialogTitle": {
"message": "Preset sources",
"description": "A button to show preset sources dialog"
},
"presetsSourcesDialogAddNew": {
"message": "Add new source",
"description": "A button to show preset sources dialog"
},
"presetsSourcesDialogDefaultSourceName": {
"message": "New Custom Preset Source",
"description": "A default preset source (repo) name"
},
"presetsSourcesDialogSaveSource": {
"message": "Save",
"description": "Presets tab, sources dialog, button to save new or editable source"
},
"presetsSourcesDialogResetSource": {
"message": "Reset",
"description": "Presets tab, sources dialog, button to reset source after modifications"
},
"presetsSourcesDialogMakeSourceActive": {
"message": "Make Active",
"description": "Presets tab, sources dialog, button to make selected source active"
},
"presetsSourcesDialogDeleteSource": {
"message": "Delete",
"description": "Presets tab, sources dialog, button to delete selected source"
},
"presetsWarningNotOfficialSource": {
"message": "<span class=\"message-negative\">WARNING!</span> A third party preset source is selected.",
"description": "Warning message that shows up when a third party preset source is selected"
},
"presetsWarningBackup": {
"message": "Please make sure you backup your current configuration ('$t(presetsBackupSave.message)' button or via CLI if the button is disabled) <strong>before</strong> picking and applying presets. Otherwise there is no way to return to previous configuration after applying presets.",
"description": "Warning message that shows up at the top of the presets tab"
},
"presets_sources_dialog_warning": {
"message": "<span class=\"message-negative\">WARNING!</span> Using third party preset sources could be dangerous.<br/>Make sure you add and use only trusted sources. Malicious or bad preset sources will break your drone configuration and can potentially harm your devices.",
"description": "Warning message that shows up at the top of the preset sources dialog"
},
"presetsWarningWrongVersionConfirmation": {
"message": "Picked preset requires firmware version $1<br/> Current firmware version is $2",
"description": "Warning message that shows up at the top of the preset sources dialog"
},
"presetsNoPresetsFound": {
"message": "No presets found for the given search paramteres",
"description": "Message that apprears on presets tab if no presets were found"
},
"presetsTooManyPresetsFound": {
"message": "Reached the maximum limit of the shown presets number",
"description": "Message that apprears on presets tab if too many presets found"
} }
} }

View file

@ -64,6 +64,7 @@
"jquery-ui-npm": "^1.12.0", "jquery-ui-npm": "^1.12.0",
"lru_map": "^0.3.3", "lru_map": "^0.3.3",
"marked": "^0.8.0", "marked": "^0.8.0",
"multiple-select": "^1.5.2",
"nw-vue-devtools-prebuilt": "^0.0.10", "nw-vue-devtools-prebuilt": "^0.0.10",
"object-hash": "^2.0.3", "object-hash": "^2.0.3",
"select2": "^4.0.13", "select2": "^4.0.13",
@ -125,7 +126,7 @@
"temp": "^0.9.1", "temp": "^0.9.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"yarn": "^1.22.0" "yarn": "^1.22.17"
}, },
"optionalDependencies": { "optionalDependencies": {
"gulp-appdmg": "^1.0.3" "gulp-appdmg": "^1.0.3"

View file

@ -111,6 +111,11 @@ dialog {
border: 1px solid #ffbb2a; border: 1px solid #ffbb2a;
} }
.standard_input {
background: var(--boxBackground);
color: white;
}
#quad-status_wrapper { #quad-status_wrapper {
color: #393b3a; color: #393b3a;
} }

View file

@ -78,6 +78,18 @@ a.disabled {
background-position: center; background-position: center;
} }
.standard_input {
padding-left: 3px;
height: 20px;
line-height: 20px;
text-align: left;
border-radius: 3px;
font-size: 12px;
font-weight: normal;
border: 1px solid var(--subtleAccent);
background: var(--boxBackground);
}
/* Help-Icon */ /* Help-Icon */
.helpicon { .helpicon {
float: right; float: right;
@ -740,8 +752,19 @@ input[type="number"]::-webkit-inner-spin-button {
background-image: url(../images/icons/cf_icon_link_active.svg); background-image: url(../images/icons/cf_icon_link_active.svg);
} }
/** Header (not phones) **/
@media not all and (max-width: 575px) {
.visible-on-phone-only {
display: none !important;
}
}
/** Header (phones) **/ /** Header (phones) **/
@media all and (max-width: 575px) { @media all and (max-width: 575px) {
.visible-on-desktop-only {
display: none !important;
}
.headerbar { .headerbar {
height: 56px; height: 56px;
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.15);
@ -1541,10 +1564,67 @@ dialog .dialog_toolbar .btn a.disabled {
opacity: 0.5; opacity: 0.5;
} }
.dialogYesNo .dialogYesNoContent {
margin-bottom: 12px;
margin-top: 12px;
white-space: pre-line;
}
.dialogYesNo .dialogYesNo-yesButton, .dialogYesNo .dialogYesNo-noButton {
margin: 0px;
}
.dialogYesNo .dialogYesNo-yesButton {
margin-right: 12px;
}
.dialogYesNo {
width: fit-content;
max-width: 400px;
}
.dialogWait {
width: fit-content;
max-width: 500px;
min-width: 300px;
}
.dialogWait .data-loading {
margin-top: 16px;
margin-bottom: 16px;
margin-left: auto;
margin-right: auto;
width: 100px;
height: 100px;
}
.dialogWait .dialogWaitTitle {
margin-left: auto;
margin-right: auto;
margin-bottom: 16px;
width: fit-content;
}
.dialogInformation .dialogInformationContent {
margin-bottom: 12px;
margin-top: 12px;
white-space: pre-line;
}
.dialogInformation .dialogInformation-confirmButton {
margin: 0px;
}
.dialogInformation {
width: fit-content;
max-width: 400px;
}
@media all and (max-width: 575px) { @media all and (max-width: 575px) {
dialog { dialog {
position: fixed; position: fixed;
width: calc(100% - 2em) !important; width: calc(100% - 2em - 2px) !important; /* 2px - border */
max-width: unset;
height: auto !important; height: auto !important;
bottom: 0; bottom: 0;
top: 56px; top: 56px;
@ -2348,7 +2428,6 @@ input {
.noUi-connect { .noUi-connect {
box-shadow: none; box-shadow: none;
} }
/** Responsive grid **/ /** Responsive grid **/
@media all and (max-width: 575px) { @media all and (max-width: 575px) {
.sm, .md, .lg, .xl { .sm, .md, .lg, .xl {

View file

@ -200,7 +200,7 @@
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: #AFAFAF; background-color: #AFAFAF;
border: none; border: 1px solid #AFAFAF;
pointer-events: none; pointer-events: none;
text-shadow: none; text-shadow: none;
opacity: 0.5; opacity: 0.5;

View file

@ -7,6 +7,8 @@
} }
.tab-options .margin-bottom { .tab-options .margin-bottom {
margin-bottom: 10px; margin-bottom: 10px;
display: grid;
grid-template-columns: fit-content(300px) 1fr;
} }
.tab-options select { .tab-options select {
background: var(--boxBackground); background: var(--boxBackground);

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<path d="M 498.25 249.524 C 498.25 386.831 386.941 498.14 249.634 498.14 C 112.327 498.14 1.018 386.831 1.018 249.524 C 1.018 112.217 112.327 0.908 249.634 0.908 C 322.597 0.908 388.219 32.339 433.7 82.399 C 334.451 142.436 269.55 216.455 213.055 312.593 L 127.757 151.63 L 54.042 218.642 L 216.387 447.914 C 305.243 286.114 350.541 188.665 450.938 103.594 C 480.699 144.577 498.25 195.001 498.25 249.524 Z" style="fill: rgb(255, 187, 0); fill-rule: nonzero; paint-order: fill;"/>
<path d="M 498.75 249.524 C 498.75 318.28 470.839 380.622 425.786 425.676 C 380.732 470.729 318.39 498.64 249.634 498.64 C 180.878 498.64 118.536 470.729 73.482 425.676 C 28.429 380.622 0.518 318.28 0.518 249.524 C 0.518 180.768 28.429 118.426 73.482 73.372 C 118.536 28.319 180.878 0.408 249.634 0.408 C 318.39 0.408 380.732 28.319 425.786 73.372 C 470.839 118.426 498.75 180.768 498.75 249.524 Z M 425.079 74.079 C 380.151 29.152 318.185 1.408 249.634 1.408 C 181.083 1.408 119.117 29.152 74.189 74.079 C 29.262 119.007 1.518 180.973 1.518 249.524 C 1.518 318.075 29.262 380.041 74.189 424.969 C 119.117 469.896 181.083 497.64 249.634 497.64 C 318.185 497.64 380.151 469.896 425.079 424.969 C 470.006 380.041 497.75 318.075 497.75 249.524 C 497.75 180.973 470.006 119.007 425.079 74.079 Z" style="fill: none;"/>
<path d="M 498.75 249.524 C 498.75 318.28 470.839 380.622 425.786 425.676 C 380.732 470.729 318.39 498.64 249.634 498.64 C 180.878 498.64 118.536 470.729 73.482 425.676 C 28.429 380.622 0.518 318.28 0.518 249.524 C 0.518 180.768 28.429 118.426 73.482 73.372 C 118.536 28.319 180.878 0.408 249.634 0.408 C 318.39 0.408 380.732 28.319 425.786 73.372 C 470.839 118.426 498.75 180.768 498.75 249.524 Z M 425.079 74.079 C 380.151 29.152 318.185 1.408 249.634 1.408 C 181.083 1.408 119.117 29.152 74.189 74.079 C 29.262 119.007 1.518 180.973 1.518 249.524 C 1.518 318.075 29.262 380.041 74.189 424.969 C 119.117 469.896 181.083 497.64 249.634 497.64 C 318.185 497.64 380.151 469.896 425.079 424.969 C 470.006 380.041 497.75 318.075 497.75 249.524 C 497.75 180.973 470.006 119.007 425.079 74.079 Z" style="fill: none;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<path d="M 499.204 185.669 C 499.204 287.561 416.604 370.161 314.712 370.161 C 212.82 370.161 130.22 287.561 130.22 185.669 C 130.22 83.777 212.82 1.177 314.712 1.177 C 416.604 1.177 499.204 83.777 499.204 185.669 Z M 314.712 48.304 C 238.662 48.304 177.012 109.954 177.012 186.004 C 177.012 262.054 238.662 323.704 314.712 323.704 C 390.762 323.704 452.412 262.054 452.412 186.004 C 452.412 109.954 390.762 48.304 314.712 48.304 Z" style="stroke-width: 50px; fill: rgb(255, 187, 0);"/>
<path d="M 4.046 435.582 L 146.371 297.269 C 161.05 321.35 181.091 341.803 204.835 356.971 L 63.129 494.665 L 4.046 435.582 Z" style="fill: rgb(255, 187, 0);"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-70 0 141.7 141.7" style="enable-background:new -70 0 141.7 141.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFBB00;}
</style>
<g>
<path class="st0" d="M8.4,34.1c0,0,16.2,7.3,21.5,13.4c0,0-5.9-0.2-8.1,3.3c-2.6,3.8-1.3,11.4,6.8,17.1s17.7,6.3,22.5,6.6
c4.8,0.2,9.6,1.3,10.6,3.5C61.4,78.1,62.9,51.6,8.4,34.1z M38.7,61.1c-1-4,2.2-5.5,6.6-3.8c4,1.8,10.6,12.1,10.6,12.1
C50.6,68.7,39.9,65.2,38.7,61.1z"/>
<path class="st0" d="M-68.6,10.4l9,19.7l-8.6-3l2.6,4.8c0,0,3.5,6.8,11.9,21.5c6.6,11.4,18.2,17.1,33.8,16.4c0.5,0,1.3,0,2,0
c9.6-0.5,31.6-3,32.6-3h0.2l10.3-4L-68.6,10.4z M-20.1,65.5c-5.5,0.2-10.6-0.5-14.9-2c-6.3-2.2-11.4-6.3-14.9-12.3
c-4.3-7.6-7.3-12.9-9-16.4l7.9,2.8L-59,20.8l74.2,40.9l-1.3,0.5c-0.5,0-1.8,0.2-3.8,0.5l-50-20.2L-2.2,64
C-9,64.4-16.4,65.2-20.1,65.5z"/>
<path class="st0" d="M24.8,59.4c0,0-31.6-8.3-26.8,16.4c3.1,16.2,21.3,6.8,27.9,0.7C32.4,70.5,27,60.2,24.8,59.4z"/>
<path class="st0" d="M41.5,96.8c-14.2-6.1-22.8-16-33.6-15.2C1.3,82.1-5.7,87.6-8.5,95c4,3.3,10.1,5.5,19.7,2c0,0-6.6,8.1-20.4,7.3
c0.2,1.8,1,3.3,1.8,5c1.3,2,3,4,5,5.5c5.9-0.7,13.9-3.5,21.7-12.1c0,0-0.7,9-7.9,17.1c3.8,0.5,7.9,0.5,12.1,0.2
c3.3-1.8,7.9-5,7.9-10.9c0,0,1.5,4.6-0.7,9.9c6.6-1.3,13.4-3.8,19.9-7.6C65.2,103,70,93.2,70,93.2S55.9,102.5,41.5,96.8z
M51.8,107.3c-0.7-2-1-6.1-1-6.1c4.6,0.5,10.1-1.5,10.1-1.5C57.2,104.6,51.8,107.3,51.8,107.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -5,30 +5,31 @@
const ConfigStorage = { const ConfigStorage = {
// key can be one string, or array of strings // key can be one string, or array of strings
get: function(key, callback) { get: function(key, callback) {
let result = {};
if (Array.isArray(key)) { if (Array.isArray(key)) {
let obj = {};
key.forEach(function (element) { key.forEach(function (element) {
try { try {
obj = {...obj, ...JSON.parse(window.localStorage.getItem(element))}; result = {...result, ...JSON.parse(window.localStorage.getItem(element))};
} catch (e) { } catch (e) {
// is okay // is okay
} }
}); });
callback(obj); callback?.(result);
} else { } else {
const keyValue = window.localStorage.getItem(key); const keyValue = window.localStorage.getItem(key);
if (keyValue) { if (keyValue) {
let obj = {};
try { try {
obj = JSON.parse(keyValue); result = JSON.parse(keyValue);
} catch (e) { } catch (e) {
// It's fine if we fail that parse // It's fine if we fail that parse
} }
callback(obj); callback?.(result);
} else { } else {
callback({}); callback?.(result);
} }
} }
return result;
}, },
// set takes an object like {'userLanguageSelect':'DEFAULT'} // set takes an object like {'userLanguageSelect':'DEFAULT'}
set: function(input) { set: function(input) {

View file

@ -30,6 +30,9 @@ const CONFIGURATOR = {
cliActive: false, cliActive: false,
cliValid: false, cliValid: false,
productName: 'Betaflight Configurator', productName: 'Betaflight Configurator',
cliEngineActive: false,
cliEngineValid: false,
gitChangesetId: 'unknown',
version: '0.0.1', version: '0.0.1',
gitRevision: 'norevision', gitRevision: 'norevision',
latestVersion: '0.0.1', latestVersion: '0.0.1',

View file

@ -19,6 +19,7 @@ const GuiControl = function () {
this.operating_system = null; this.operating_system = null;
this.interval_array = []; this.interval_array = [];
this.timeout_array = []; this.timeout_array = [];
this.buttonDisabledClass = "disabled";
this.defaultAllowedTabsWhenDisconnected = [ this.defaultAllowedTabsWhenDisconnected = [
'landing', 'landing',
@ -36,6 +37,7 @@ const GuiControl = function () {
'power', 'power',
'adjustments', 'adjustments',
'auxiliary', 'auxiliary',
'presets',
'cli', 'cli',
'configuration', 'configuration',
'gps', 'gps',
@ -407,5 +409,166 @@ GuiControl.prototype.isOther = function () {
return this.Mode === GUI_MODES.Other; return this.Mode === GUI_MODES.Other;
}; };
GuiControl.prototype.showYesNoDialog = function(yesNoDialogSettings) {
// yesNoDialogSettings:
// title, text, buttonYesText, buttonNoText, buttonYesCallback, buttonNoCallback
const dialog = $(".dialogYesNo");
const title = dialog.find(".dialogYesNoTitle");
const content = dialog.find(".dialogYesNoContent");
const buttonYes = dialog.find(".dialogYesNo-yesButton");
const buttonNo = dialog.find(".dialogYesNo-noButton");
title.html(yesNoDialogSettings.title);
content.html(yesNoDialogSettings.text);
buttonYes.html(yesNoDialogSettings.buttonYesText);
buttonNo.html(yesNoDialogSettings.buttonNoText);
buttonYes.off("click");
buttonNo.off("click");
buttonYes.on("click", () => {
dialog[0].close();
yesNoDialogSettings.buttonYesCallback?.();
});
buttonNo.on("click", () => {
dialog[0].close();
yesNoDialogSettings.buttonNoCallback?.();
});
dialog[0].showModal();
};
GuiControl.prototype.showWaitDialog = function(waitDialogSettings) {
// waitDialogSettings:
// title, buttonCancelCallback
const dialog = $(".dialogWait")[0];
const title = $(".dialogWaitTitle");
const buttonCancel = $(".dialogWait-cancelButton");
title.html(waitDialogSettings.title);
buttonCancel.toggle(!!waitDialogSettings.buttonCancelCallback);
buttonCancel.off("click");
buttonCancel.on("click", () => {
dialog.close();
waitDialogSettings.buttonCancelCallback?.();
});
dialog.showModal();
return dialog;
};
GuiControl.prototype.showInformationDialog = function(informationDialogSettings) {
// informationDialogSettings:
// title, text, buttonConfirmText
return new Promise(resolve => {
const dialog = $(".dialogInformation");
const title = dialog.find(".dialogInformationTitle");
const content = dialog.find(".dialogInformationContent");
const buttonConfirm = dialog.find(".dialogInformation-confirmButton");
title.html(informationDialogSettings.title);
content.html(informationDialogSettings.text);
buttonConfirm.html(informationDialogSettings.buttonConfirmText);
buttonConfirm.off("click");
buttonConfirm.on("click", () => {
dialog[0].close();
resolve();
});
dialog[0].showModal();
});
};
GuiControl.prototype.saveToTextFileDialog = function(textToSave, suggestedFileName, extension) {
return new Promise((resolve, reject) => {
const accepts = [{ description: extension.toUpperCase() + ' files', extensions: [extension] }];
chrome.fileSystem.chooseEntry(
{
type: 'saveFile',
suggestedName: suggestedFileName,
accepts: accepts,
},
entry => this._saveToTextFileDialogFileSelected(entry, textToSave, resolve, reject),
);
});
};
GuiControl.prototype._saveToTextFileDialogFileSelected = function(entry, textToSave, resolve, reject) {
checkChromeRuntimeError();
if (!entry) {
console.log('No file selected for saving');
resolve(false);
return;
}
entry.createWriter(writer => {
writer.onerror = () => {
reject();
console.error('Failed to write file');
};
writer.onwriteend = () => {
if (textToSave.length > 0 && writer.length === 0) {
writer.write(new Blob([textToSave], {type: 'text/plain'}));
} else {
resolve(true);
console.log('File write complete');
}
};
writer.truncate(0);
},
() => {
reject();
console.error('Failed to get file writer');
});
};
GuiControl.prototype.readTextFileDialog = function(extension) {
const accepts = [{ description: extension.toUpperCase() + ' files', extensions: [extension] }];
return new Promise(resolve => {
chrome.fileSystem.chooseEntry({type: 'openFile', accepts: accepts}, function(entry) {
checkChromeRuntimeError();
if (!entry) {
console.log('No file selected for loading');
resolve(false);
return;
}
entry.file((file) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => {
console.error(reader.error);
reject();
};
reader.readAsText(file);
});
});
});
};
GuiControl.prototype.escapeHtml = function(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
// initialize object into GUI variable // initialize object into GUI variable
window.GUI = new GuiControl(); window.GUI = new GuiControl();

View file

@ -401,6 +401,9 @@ function startProcess() {
case 'cli': case 'cli':
TABS.cli.initialize(content_ready, GUI.nwGui); TABS.cli.initialize(content_ready, GUI.nwGui);
break; break;
case 'presets':
TABS.presets.initialize(content_ready, GUI.nwGui);
break;
default: default:
console.log(`Tab not found: ${tab}`); console.log(`Tab not found: ${tab}`);

View file

@ -583,13 +583,17 @@ function onClosed(result) {
CONFIGURATOR.connectionValid = false; CONFIGURATOR.connectionValid = false;
CONFIGURATOR.cliValid = false; CONFIGURATOR.cliValid = false;
CONFIGURATOR.cliActive = false; CONFIGURATOR.cliActive = false;
CONFIGURATOR.cliEngineValid = false;
CONFIGURATOR.cliEngineActive = false;
} }
function read_serial(info) { function read_serial(info) {
if (!CONFIGURATOR.cliActive) { if (CONFIGURATOR.cliActive) {
MSP.read(info);
} else if (CONFIGURATOR.cliActive) {
TABS.cli.read(info); TABS.cli.read(info);
} else if (CONFIGURATOR.cliEngineActive) {
TABS.presets.read(info);
} else {
MSP.read(info);
} }
} }
@ -694,7 +698,8 @@ function update_live_status() {
display: 'inline-block' display: 'inline-block'
}); });
if (GUI.active_tab != 'cli') { if (GUI.active_tab !== 'cli' && GUI.active_tab !== 'presets') {
MSP.send_message(MSPCodes.MSP_BOXNAMES, false, false);
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_32)) { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_32)) {
MSP.send_message(MSPCodes.MSP_STATUS_EX, false, false); MSP.send_message(MSPCodes.MSP_STATUS_EX, false, false);
} else { } else {

View file

@ -18,6 +18,8 @@ options.initialize = function (callback) {
TABS.options.initCordovaForceComputerUI(); TABS.options.initCordovaForceComputerUI();
TABS.options.initDarkTheme(); TABS.options.initDarkTheme();
TABS.options.initShowWarnings();
GUI.content_ready(callback); GUI.content_ready(callback);
}); });
}; };
@ -28,6 +30,19 @@ options.cleanup = function (callback) {
} }
}; };
options.initShowWarnings = function () {
ConfigStorage.get('showPresetsWarningBackup', function (result) {
if (result.showPresetsWarningBackup) {
$('div.presetsWarningBackup input').prop('checked', true);
}
$('div.presetsWarningBackup input').change(function () {
const checked = $(this).is(':checked');
ConfigStorage.set({'showPresetsWarningBackup': checked});
}).change();
});
};
options.initPermanentExpertMode = function () { options.initPermanentExpertMode = function () {
ConfigStorage.get('permanentExpertMode', function (result) { ConfigStorage.get('permanentExpertMode', function (result) {
if (result.permanentExpertMode) { if (result.permanentExpertMode) {

View file

@ -22,6 +22,11 @@
<link type="text/css" rel="stylesheet" href="./css/tabs/led_strip.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/led_strip.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/tabs/sensors.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/sensors.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/tabs/cli.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/cli.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./tabs/presets/presets.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./tabs/presets/TitlePanel/PresetTitlePanel.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./tabs/presets/DetailedDialog/PresetsDetailedDialog.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./tabs/presets/SourcesDialog/SourcesDialog.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./tabs/presets/SourcesDialog/SourcePanel.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/tabs/logging.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/logging.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/tabs/onboard_logging.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/onboard_logging.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/tabs/firmware_flasher.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/tabs/firmware_flasher.css" media="all"/>
@ -42,6 +47,7 @@
<link type="text/css" rel="stylesheet" href="./components/MotorOutputReordering/Styles.css" media="all"/> <link type="text/css" rel="stylesheet" href="./components/MotorOutputReordering/Styles.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/select2_custom.css" media="all"/> <link type="text/css" rel="stylesheet" href="./css/select2_custom.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./node_modules/select2/dist/css/select2.min.css" media="all"/> <link type="text/css" rel="stylesheet" href="./node_modules/select2/dist/css/select2.min.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./node_modules/multiple-select/dist/multiple-select.min.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./components/EscDshotDirection/Styles.css" media="all"/> <link type="text/css" rel="stylesheet" href="./components/EscDshotDirection/Styles.css" media="all"/>
<link type="text/css" rel="stylesheet" href="./css/dark-theme.css" media="all" disabled/> <link type="text/css" rel="stylesheet" href="./css/dark-theme.css" media="all" disabled/>
@ -126,6 +132,18 @@
<script type="text/javascript" src="./js/tabs/cli.js"></script> <script type="text/javascript" src="./js/tabs/cli.js"></script>
<!-- TODO: might be removed when everythign is in modules --> <!-- TODO: might be removed when everythign is in modules -->
<script type="module" src="./js/tabs/logging.js"></script> <script type="module" src="./js/tabs/logging.js"></script>
<script type="text/javascript" src="./tabs/presets/presets.js"></script>
<script type="text/javascript" src="./tabs/presets/CliEngine.js"></script>
<script type="text/javascript" src="./tabs/presets/PickedPreset.js"></script>
<script type="text/javascript" src="./tabs/presets/DetailedDialog/PresetsDetailedDialog.js"></script>
<script type="text/javascript" src="./tabs/presets/TitlePanel/PresetTitlePanel.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsGithubRepo.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsWebsiteRepo.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/SourcesDialog.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/SourcePanel.js"></script>
<script type="text/javascript" src="./tabs/presets/SourcesDialog/PresetSource.js"></script>
<script type="text/javascript" src="./js/tabs/logging.js"></script>
<script type="text/javascript" src="./js/tabs/onboard_logging.js"></script> <script type="text/javascript" src="./js/tabs/onboard_logging.js"></script>
<script type="text/javascript" src="./js/FirmwareCache.js"></script> <script type="text/javascript" src="./js/FirmwareCache.js"></script>
<script type="text/javascript" src="./js/tabs/failsafe.js"></script> <script type="text/javascript" src="./js/tabs/failsafe.js"></script>
@ -145,6 +163,7 @@
<script type="text/javascript" src="./components/MotorOutputReordering/MotorOutputReorderingCanvas.js"></script> <script type="text/javascript" src="./components/MotorOutputReordering/MotorOutputReorderingCanvas.js"></script>
<script type="text/javascript" src="./components/MotorOutputReordering/MotorOutputReorderingConfig.js"></script> <script type="text/javascript" src="./components/MotorOutputReordering/MotorOutputReorderingConfig.js"></script>
<script type="text/javascript" src="./node_modules/select2/dist/js/select2.min.js"></script> <script type="text/javascript" src="./node_modules/select2/dist/js/select2.min.js"></script>
<script type="text/javascript" src="./node_modules/multiple-select/dist/multiple-select.min.js"></script>
<script type="text/javascript" src="./js/utils/EscProtocols.js"></script> <script type="text/javascript" src="./js/utils/EscProtocols.js"></script>
<script type="text/javascript" src="./js/utils/DshotCommand.js"></script> <script type="text/javascript" src="./js/utils/DshotCommand.js"></script>
<script type="text/javascript" src="./components/EscDshotDirection/EscDshotDirectionComponent.js"></script> <script type="text/javascript" src="./components/EscDshotDirection/EscDshotDirectionComponent.js"></script>
@ -326,6 +345,9 @@
i18n_title="tabPower"></a></li> i18n_title="tabPower"></a></li>
<li class="tab_failsafe"><a href="#" i18n="tabFailsafe" class="tabicon ic_failsafe" <li class="tab_failsafe"><a href="#" i18n="tabFailsafe" class="tabicon ic_failsafe"
i18n_title="tabFailsafe"></a></li> i18n_title="tabFailsafe"></a></li>
<li class="tab_presets">
<a href="#" i18n="tabPresets" class="tabicon ic_wizzard" i18n_title="tabPresets"></a>
</li>
<li class="tab_pid_tuning"><a href="#" i18n="tabPidTuning" class="tabicon ic_pid" <li class="tab_pid_tuning"><a href="#" i18n="tabPidTuning" class="tabicon ic_pid"
i18n_title="tabPidTuning"></a></li> i18n_title="tabPidTuning"></a></li>
<li class="tab_receiver"><a href="#" i18n="tabReceiver" class="tabicon ic_rx" i18n_title="tabReceiver"></a></li> <li class="tab_receiver"><a href="#" i18n="tabReceiver" class="tabicon ic_rx" i18n_title="tabReceiver"></a></li>
@ -462,6 +484,30 @@
</div> </div>
</dialog> </dialog>
<dialog class="dialogYesNo">
<h3 class="dialogYesNoTitle"></h3>
<div class="dialogYesNoContent"></div>
<div class="buttons">
<a href="#" class="dialogYesNo-yesButton regular-button"></a>
<a href="#" class="dialogYesNo-noButton regular-button"></a>
</div>
</dialog>
<dialog class="dialogWait">
<div class="data-loading"></div>
<h3 class="dialogWaitTitle"></h3>
<div class="buttons">
<a href="#" class="dialogWait-cancelButton regular-button" i18n="cancel"></a>
</div>
</dialog>
<dialog class="dialogInformation">
<h3 class="dialogInformationTitle"></h3>
<div class="dialogInformationContent"></div>
<div class="buttons">
<a href="#" class="dialogInformation-confirmButton regular-button"></a>
</div>
</dialog>
<!-- CORDOVA_INCLUDE cordova.js --> <!-- CORDOVA_INCLUDE cordova.js -->
</body> </body>
</html> </html>

View file

@ -62,5 +62,20 @@
</div> </div>
</div> </div>
</div> </div>
<div class="gui_box">
<div class="gui_box_titlebar">
<div class="spacer_box_title" i18n="warningSettings"></div>
</div>
<div class="spacer">
<div class="presetsWarningBackup margin-bottom">
<div>
<input type="checkbox" class="toggle" />
</div>
<span class="freelabel" i18n="presetsWarningBackup"></span>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,275 @@
'use strict';
class CliEngine
{
constructor(currentTab) {
this._currentTab = currentTab;
this._lineDelayMs = 15;
this._profileSwitchDelayMs = 100;
this._cliBuffer = "";
this._window = null;
this._windowWrapper = null;
this._cliErrorsCount = 0;
this._sendCommandsProgress = 0;
this._onSendCommandsProgressChange = undefined;
this._responseCallback = undefined;
this._onRowCameCallback = undefined;
}
setUi(window, windowWrapper, textarea) {
this._window = window;
this._windowWrapper = windowWrapper;
this._setTextareaListen(textarea);
}
get errorsCount() { return this._cliErrorsCount; }
setProgressCallback(sendCommandsProgressCallBack) {
this._onSendCommandsProgressChange = sendCommandsProgressCallBack;
}
_reportSendCommandsProgress(value) {
this._sendCommandsProgress = value;
this._onSendCommandsProgressChange?.(value);
}
enterCliMode() {
const bufferOut = new ArrayBuffer(1);
const bufView = new Uint8Array(bufferOut);
this.cliBuffer = "";
bufView[0] = 0x23;
serial.send(bufferOut);
}
_setTextareaListen(textarea) {
// Tab key detection must be on keydown,
// `keypress`/`keyup` happens too late, as `textarea` will have already lost focus.
textarea.keydown(event => {
if (event.which === CliEngine.s_tabCode) {
// prevent default tabbing behaviour
event.preventDefault();
}
});
textarea.keypress(event => {
if (event.which === CliEngine.s_enterKeyCode) {
event.preventDefault(); // prevent the adding of new line
const outString = textarea.val();
this.executeCommands(outString);
textarea.val('');
}
});
// give input element user focus
textarea.focus();
}
close(callback) {
this.send(this.getCliCommand('exit\r', ""), function () { //this.cliBuffer
if (callback) {
callback();
}
});
}
executeCommands(outString) {
const outputArray = outString.split("\n");
return this.executeCommandsArray(outputArray);
}
executeCommandsArray(strings) {
this._reportSendCommandsProgress(0);
const totalCommandsCount = strings.length;
return Promise.reduce(strings, (delay, line, index) => {
return new Promise((resolve) => {
GUI.timeout_add('CLI_send_slowly', () => {
let processingDelay = this.lineDelayMs;
line = line.trim();
if (line.toLowerCase().startsWith('profile')) {
processingDelay = this.profileSwitchDelayMs;
}
const isLastCommand = totalCommandsCount === index + 1;
if (isLastCommand && this.cliBuffer) {
line = this.getCliCommand(line, this.cliBuffer);
}
this.sendLine(line, ()=>{ /* empty on-send callback */ }, () => {
resolve(processingDelay);
this._reportSendCommandsProgress(100.0 * index / totalCommandsCount);
});
}, delay);
});
}, 0).then(() => {
this._reportSendCommandsProgress(100);
});
}
removePromptHash(promptText) {
return promptText.replace(/^# /, '');
}
cliBufferCharsToDelete(command, buffer) {
let commonChars = 0;
for (let i = 0; i < buffer.length; i++) {
if (command[i] === buffer[i]) {
commonChars++;
} else {
break;
}
}
return buffer.length - commonChars;
}
commandWithBackSpaces(command, buffer, noOfCharsToDelete) {
const backspace = String.fromCharCode(127);
return backspace.repeat(noOfCharsToDelete) + command.substring(buffer.length - noOfCharsToDelete, command.length);
}
getCliCommand(command, cliBuffer) {
const buffer = this.removePromptHash(cliBuffer);
const bufferRegex = new RegExp('^' + buffer, 'g');
if (command.match(bufferRegex)) {
return command.replace(bufferRegex, '');
}
const noOfCharsToDelete = this.cliBufferCharsToDelete(command, buffer);
return this.commandWithBackSpaces(command, buffer, noOfCharsToDelete);
}
writeToOutput(text) {
this._windowWrapper.append(text);
this._window.scrollTop(this._windowWrapper.height());
}
writeLineToOutput(text) {
if (text.startsWith("###ERROR")) {
this.writeToOutput(`<span class="error_message">${text}</span><br>`);
this._cliErrorsCount++;
} else {
this.writeToOutput(text + "<br>");
}
this._responseCallback?.();
this._onRowCameCallback?.(text);
}
subscribeOnRowCame(callback) {
this._onRowCameCallback = callback;
}
unsubscribeOnRowCame() {
this._onRowCameCallback = undefined;
}
readSerial(readInfo) {
/* Some info about handling line feeds and carriage return
line feed = LF = \n = 0x0A = 10
carriage return = CR = \r = 0x0D = 13
MAC only understands CR
Linux and Unix only understand LF
Windows understands (both) CRLF
Chrome OS currently unknown
*/
const data = new Uint8Array(readInfo.data);
let validateText = "";
let sequenceCharsToSkip = 0;
for (const charCode of data) {
const currentChar = String.fromCharCode(charCode);
if (!CONFIGURATOR.cliEngineValid) {
// try to catch part of valid CLI enter message
validateText += currentChar;
this.writeToOutput(currentChar);
continue;
}
const escapeSequenceCode = 27;
const escapeSequenceCharLength = 3;
if (charCode === escapeSequenceCode && !sequenceCharsToSkip) { // ESC + other
sequenceCharsToSkip = escapeSequenceCharLength;
}
if (sequenceCharsToSkip) {
sequenceCharsToSkip--;
continue;
}
this._adjustCliBuffer(charCode);
if (this.cliBuffer === 'Rebooting' && CliEngine.s_backspaceCode !== charCode) {
CONFIGURATOR.cliEngineActive = false;
CONFIGURATOR.cliEngineValid = false;
GUI.log(i18n.getMessage('cliReboot'));
reinitialiseConnection(this._currentTab);
}
}
if (!CONFIGURATOR.cliEngineValid && validateText.indexOf('CLI') !== -1) {
GUI.log(i18n.getMessage('cliEnter'));
CONFIGURATOR.cliEngineValid = true;
}
}
sendLine(line, callback, responseCallback) {
this.send(line + '\n', callback, responseCallback);
}
send(line, callback, responseCallback) {
this._responseCallback = responseCallback;
const bufferOut = new ArrayBuffer(line.length);
const bufView = new Uint8Array(bufferOut);
for (let cKey = 0; cKey < line.length; cKey++) {
bufView[cKey] = line.charCodeAt(cKey);
}
serial.send(bufferOut, callback);
}
_adjustCliBuffer(newCharacterCode) {
const currentChar = String.fromCharCode(newCharacterCode);
switch (newCharacterCode) {
case CliEngine.s_lineFeedCode:
if (GUI.operating_system === "Windows") {
this.writeLineToOutput(this.cliBuffer);
this.cliBuffer = "";
}
break;
case CliEngine.s_carriageReturnCode:
if (GUI.operating_system !== "Windows") {
this.writeLineToOutput(this.cliBuffer);
this.cliBuffer = "";
}
break;
case 60:
this.cliBuffer += '&lt';
break;
case 62:
this.cliBuffer += '&gt';
break;
case CliEngine.s_backspaceCode:
this.cliBuffer = this.cliBuffer.slice(0, -1);
break;
default:
this.cliBuffer += currentChar;
}
}
}
CliEngine.s_backspaceCode = 8;
CliEngine.s_lineFeedCode = 10;
CliEngine.s_carriageReturnCode = 13;
CliEngine.s_tabCode = 9;
CliEngine.s_enterKeyCode = 13;
CliEngine.s_commandDiffAll = "diff all";
CliEngine.s_commandDefaultsNoSave = "defaults nosave";
CliEngine.s_commandSave = "save";
CliEngine.s_commandExit = "exit";

View file

@ -0,0 +1,112 @@
.preset_detailed_dialog_title {
padding-left: 10px;
padding-right: 10px;
}
#presets_detailed_dialog_content_wrapper .preset_title_panel_title {
padding-bottom: .5ex;
border-bottom: 1px solid var(--accent);
margin-bottom: 2ex;
}
.presets-detailed-dialog-property-table {
margin-top: 10px;
margin-bottom: 10px;
}
.presets-detailed-dialog-property-table td {
padding-left: 10px;
padding-right: 10px;
padding-top: 6px;
padding-bottom: 6px;
}
.presets-detailed-dialog-property-table-first-col {
width: 150px;
}
.presets_detailed_dialog_text {
padding-top: 6px;
padding-bottom: 6px;
margin-top: 12px;
overflow-y: scroll;
height: 270px;
font-size: 110%;
white-space: pre-line;
user-select: text;
}
#presets_detailed_dialog_properties {
height: 360px;
}
#presets_detailed_dialog_content_wrapper .left-panel {
position: absolute;
left: 0px;
padding-left: 20px;
}
#presets_detailed_dialog_loading {
height: 300px;
}
#presets_options_select {
width: 296px;
}
#presets_options_panel {
height: 26px;
margin-top: 6px;
}
#presets_options_panel > span{
line-height: 26px;
}
#preset_options_label {
width : 100px;
display: inline-block;
}
@media all and (max-width: 575px) {
.presets_detailed_dialog_text {
height: unset;
padding-bottom: 100px;
}
#presets_detailed_dialog .content_toolbar {
position: fixed;
height: 70px;
}
#presets_options_panel {
height: 26px;
margin-top: 6px;
grid-template-columns: 100px 1fr;
display: grid;
}
#presets_options_select {
width: 100%;
}
#presets_detailed_dialog .btn {
margin-left: 0px;
width: 100%;
}
#presets_detailed_dialog .btn .left-panel {
margin-left: 12px;
margin-bottom: 12px;
padding-left: 0px;
}
#presets_detailed_dialog .btn .left-panel .regular-button {
float: none;
display: inline-block;
}
#presets_detailed_dialog .btn .regular-button:not(#presets_detailed_dialog .btn .left-panel .regular-button) {
margin-top: 40px;
}
}

View file

@ -0,0 +1,28 @@
<div id="presets_detailed_dialog_content_wrapper">
<div id="presets_detailed_dialog_content">
<div id="presets_detailed_dialog_properties">
<div class="preset_detailed_dialog_title_panel"></div>
<div id="presets_options_panel">
<span id="preset_options_label" i18n="presetsOptions"></span>
<select multiple="multiple" id="presets_options_select"></select>
</div>
<div class="presets_detailed_dialog_text" id="presets_detailed_dialog_text_description"></div>
<div class="presets_detailed_dialog_text" id="presets_detailed_dialog_text_cli"></div>
</div>
<div id = "presets_detailed_dialog_loading" class="data-loading"></div>
<div id = "presets_detailed_dialog_error"></div>
</div>
<div class="content_toolbar">
<div class="btn">
<div class="left-panel">
<a id="presets_cli_show" i18n="presetsShowCli" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
<a id="presets_cli_hide" i18n="presetsHideCli" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
<a id="presets_open_online" i18n="presetsViewOnline" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
<a id="presets_open_discussion" i18n="presetsOpenDiscussion" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
</div>
<a href="#" id="presets_detailed_dialog_applybtn" class="tool regular-button" i18n="presetsApply"></a>
<a href="#" id="presets_detailed_dialog_closebtn" class="tool regular-button" i18n="close"></a>
</div>
</div>
</div>

View file

@ -0,0 +1,223 @@
'use strict';
class PresetsDetailedDialog {
constructor(domDialog, pickedPresetList, onPresetPickedCallback) {
this._domDialog = domDialog;
this._pickedPresetList = pickedPresetList;
this._finalDialogYesNoSettings = {};
this._onPresetPickedCallback = onPresetPickedCallback;
this._openPromiseResolve = undefined;
}
load() {
return new Promise(resolve => {
this._domDialog.load("./tabs/presets/DetailedDialog/PresetsDetailedDialog.html", () => {
this._setupdialog();
resolve();
});
});
}
open(preset, presetsRepo) {
this._presetsRepo = presetsRepo;
this._preset = preset;
this._setLoadingState(true);
this._domDialog[0].showModal();
this._presetsRepo.loadPreset(this._preset)
.then(() => {
this._loadPresetUi();
this._setLoadingState(false);
this._setFinalYesNoDialogSettings();
})
.catch(err => {
console.error(err);
const msg = i18n.getMessage("presetsLoadError");
this._showError(msg);
});
return new Promise(resolve => this._openPromiseResolve = resolve);
}
_setFinalYesNoDialogSettings() {
this._finalDialogYesNoSettings = {
title: i18n.getMessage("presetsWarningDialogTitle"),
text: GUI.escapeHtml(this._preset.completeWarning),
buttonYesText: i18n.getMessage("presetsWarningDialogYesButton"),
buttonNoText: i18n.getMessage("presetsWarningDialogNoButton"),
buttonYesCallback: () => this._pickPresetFwVersionCheck(),
buttonNoCallback: null,
};
}
_getFinalCliText() {
const optionsToInclude = this._domOptionsSelect.multipleSelect("getSelects", "text");
return this._presetsRepo.removeUncheckedOptions(this._preset.originalPresetCliStrings, optionsToInclude);
}
_loadPresetUi() {
this._domDescription.text(this._preset.description?.join("\n"));
this._domGitHubLink.attr("href", this._presetsRepo.getPresetOnlineLink(this._preset));
if (this._preset.discussion) {
this._domDiscussionLink.removeClass(GUI.buttonDisabledClass);
this._domDiscussionLink.attr("href", this._preset.discussion);
} else{
this._domDiscussionLink.addClass(GUI.buttonDisabledClass);
}
this._titlePanel.empty();
const titlePanel = new PresetTitlePanel(this._titlePanel, this._preset, false, () => this._setLoadingState(false));
titlePanel.load();
this._loadOptionsSelect();
this._updateFinalCliText();
this._showCliText(false);
}
_updateFinalCliText() {
this._domCliText.text(this._getFinalCliText().join("\n"));
}
_setLoadingState(isLoading) {
this._domProperties.toggle(!isLoading);
this._domLoading.toggle(isLoading);
this._domError.toggle(false);
if (isLoading) {
this._domButtonApply.addClass(GUI.buttonDisabledClass);
} else {
this._domButtonApply.removeClass(GUI.buttonDisabledClass);
}
}
_showError(msg) {
this._domError.toggle(true);
this._domError.text(msg);
this._domProperties.toggle(false);
this._domLoading.toggle(false);
this._domButtonApply.addClass(GUI.buttonDisabledClass);
}
_readDom() {
this._domButtonApply = $('#presets_detailed_dialog_applybtn');
this._domButtonCancel = $('#presets_detailed_dialog_closebtn');
this._domLoading = $('#presets_detailed_dialog_loading');
this._domError = $('#presets_detailed_dialog_error');
this._domProperties = $('#presets_detailed_dialog_properties');
this._titlePanel = $('.preset_detailed_dialog_title_panel');
this._domDescription = $('#presets_detailed_dialog_text_description');
this._domCliText = $('#presets_detailed_dialog_text_cli');
this._domGitHubLink = this._domDialog.find('#presets_open_online');
this._domDiscussionLink = this._domDialog.find('#presets_open_discussion');
this._domOptionsSelect = $('#presets_options_select');
this._domOptionsSelectPanel = $('#presets_options_panel');
this._domButtonCliShow = $('#presets_cli_show');
this._domButtonCliHide = $('#presets_cli_hide');
}
_showCliText(value) {
this._domDescription.toggle(!value);
this._domCliText.toggle(value);
this._domButtonCliShow.toggle(!value);
this._domButtonCliHide.toggle(value);
}
_createOptionsSelect(options) {
options.forEach(option => {
let selectedString = "selected=\"selected\"";
if (!option.checked) {
selectedString = "";
}
this._domOptionsSelect.append(`<option value="${option.name}" ${selectedString}>${option.name}</option>`);
});
this._domOptionsSelect.multipleSelect({
placeholder: i18n.getMessage("dropDownAll"),
formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); },
formatAllSelected() { return i18n.getMessage("dropDownAll"); },
onClick: () => this._optionsSelectionChanged(),
onCheckAll: () => this._optionsSelectionChanged(),
onUncheckAll: () => this._optionsSelectionChanged(),
});
}
_optionsSelectionChanged() {
this._updateFinalCliText();
}
_destroyOptionsSelect() {
this._domOptionsSelect.multipleSelect('destroy');
}
_loadOptionsSelect() {
const optionsVisible = 0 !== this._preset.options.length;
this._domOptionsSelect.empty();
this._domOptionsSelectPanel.toggle(optionsVisible);
if (optionsVisible) {
this._createOptionsSelect(this._preset.options);
}
this._domOptionsSelect.multipleSelect('refresh');
}
_setupdialog() {
i18n.localizePage();
this._readDom();
this._domButtonApply.on("click", () => this._onApplyButtonClicked());
this._domButtonCancel.on("click", () => this._onCancelButtonClicked(false));
this._domButtonCliShow.on("click", () => this._showCliText(true));
this._domButtonCliHide.on("click", () => this._showCliText(false));
}
_onApplyButtonClicked() {
if (!this._preset.completeWarning) {
this._pickPresetFwVersionCheck();
} else {
GUI.showYesNoDialog(this._finalDialogYesNoSettings);
}
}
_pickPreset() {
const cliStrings = this._getFinalCliText();
const pickedPreset = new PickedPreset(this._preset, cliStrings);
this._pickedPresetList.push(pickedPreset);
this._onPresetPickedCallback?.();
this._onCancelButtonClicked(true);
}
_pickPresetFwVersionCheck() {
let compatitable = false;
for (const fw of this._preset.firmware_version) {
if (FC.CONFIG.flightControllerVersion.startsWith(fw)) {
compatitable = true;
break;
}
}
if (compatitable) {
this._pickPreset();
} else {
const dialogSettings = {
title: i18n.getMessage("presetsWarningDialogTitle"),
text: i18n.getMessage("presetsWarningWrongVersionConfirmation", [this._preset.firmware_version, FC.CONFIG.flightControllerVersion]),
buttonYesText: i18n.getMessage("presetsWarningDialogYesButton"),
buttonNoText: i18n.getMessage("presetsWarningDialogNoButton"),
buttonYesCallback: () => this._pickPreset(),
buttonNoCallback: null,
};
GUI.showYesNoDialog(dialogSettings);
}
}
_onCancelButtonClicked(isPresetPicked) {
this._destroyOptionsSelect();
this._domDialog[0].close();
this._openPromiseResolve?.(isPresetPicked);
}
}

View file

@ -0,0 +1,10 @@
'use strict';
class PickedPreset
{
constructor(preset, presetCli)
{
this.preset = preset;
this.presetCli = presetCli;
}
}

View file

@ -0,0 +1,26 @@
'use strict';
class PresetsGithubRepo extends PresetsRepoIndexed {
constructor(urlRepo, branch) {
let correctUrlRepo = urlRepo.trim();
if (!correctUrlRepo.endsWith("/")) {
correctUrlRepo += "/";
}
let correctBranch = branch.trim();
if (correctBranch.startsWith("/")) {
correctBranch = correctBranch.slice(1);
}
if (correctBranch.endsWith("/")) {
correctBranch = correctBranch.slice(0, -1);
}
const urlRaw = `https://raw.githubusercontent.com${correctUrlRepo.slice("https://github.com".length)}${correctBranch}/`;
const urlViewOnline = `${correctUrlRepo}blob/${correctBranch}/`;
super(urlRaw, urlViewOnline);
}
}

View file

@ -0,0 +1,223 @@
'use strict';
class PresetsRepoIndexed {
constructor(urlRaw, urlViewOnline) {
this._urlRaw = urlRaw;
this._urlViewOnline = urlViewOnline;
this._index = null;
}
get index() {
return this._index;
}
loadIndex() {
return fetch(this._urlRaw + "index.json")
.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();
}
getPresetOnlineLink(preset) {
return this._urlViewOnline + preset.fullPath;
}
_parceInclude(strings, includeRowIndexes, promises)
{
for (let i = 0; i < strings.length; i++) {
const match = PresetsRepoIndexed._sRegExpInclude.exec(strings[i].toLowerCase());
if (match !== null) {
includeRowIndexes.push(i);
const filePath = this._urlRaw + match.groups.filePath;
const promise = this._loadPresetText(filePath);
promises.push(promise);
}
}
}
_executeIncludeOnce(strings) {
const includeRowIndexes = []; // row indexes with "#include" statements
const promises = []; // promises to load included files
this._parceInclude(strings, includeRowIndexes, promises);
return Promise.all(promises)
.then(includedTexts => {
for (let i = 0; i < includedTexts.length; i++) {
strings[includeRowIndexes[i]] = includedTexts[i];
}
const text = strings.join('\n');
return text.split("\n").map(str => str.trim());
});
}
_executeIncludeNested(strings) {
const isIncludeFound = this._isIncludeFound(strings);
if (isIncludeFound) {
return this._executeIncludeOnce(strings)
.then(resultStrings => this._executeIncludeNested(resultStrings));
} else {
return new Promise.resolve(strings);
}
}
_isIncludeFound(strings) {
for (const str of strings) {
const match = PresetsRepoIndexed._sRegExpInclude.exec(str.toLowerCase());
if (match !== null) {
return true;
}
}
return false;
}
loadPreset(preset) {
const promiseMainText = this._loadPresetText(this._urlRaw + preset.fullPath);
return promiseMainText
.then(text => {
let strings = text.split("\n");
strings = strings.map(str => str.trim());
return strings;
})
.then(strings => this._executeIncludeNested(strings))
.then(strings => {
preset.originalPresetCliStrings = strings;
return this._loadPresetWarning(preset);
});
}
_loadPresetWarning(preset) {
let completeWarning = "";
if (preset.warning) {
completeWarning += (completeWarning?"\n":"") + preset.warning;
}
if (preset.disclaimer) {
completeWarning += (completeWarning?"\n":"") + preset.disclaimer;
}
const allFiles = [].concat(...[preset.include_warning, preset.include_disclaimer].filter(Array.isArray));
return this._loadFilesInOneText(allFiles)
.then(text => {
completeWarning += (completeWarning?"\n":"") + text;
preset.completeWarning = completeWarning;
});
}
_loadFilesInOneText(fileUrls) {
const loadPromises = [];
fileUrls?.forEach(url => {
const filePath = this._urlRaw + url;
loadPromises.push(this._loadPresetText(filePath));
});
return Promise.all(loadPromises)
.then(texts => {
return texts.join('\n');
});
}
_loadPresetText(fullUrl) {
return new Promise((resolve, reject) => {
fetch(fullUrl)
.then(res => res.text())
.then(text => resolve(text))
.catch(err => {
console.error(err);
reject(err);
});
});
}
}
PresetsRepoIndexed._sCliCommentDirective = "#";
PresetsRepoIndexed._sCliAttributeDirective = "#$";
// Reg exp extracts file/path.txt from # include: file/path.txt
PresetsRepoIndexed._sRegExpInclude = /^#\$[ ]+?include:[ ]+?(?<filePath>\S+$)/;

View file

@ -0,0 +1,16 @@
'use strict';
class PresetsWebsiteRepo extends PresetsRepoIndexed {
constructor(url) {
let correctUrl = url.trim();
if (!correctUrl.endsWith("/")) {
correctUrl += "/";
}
const urlRaw = correctUrl;
const urlViewOnline = correctUrl;
super(urlRaw, urlViewOnline);
}
}

View file

@ -0,0 +1,14 @@
'use strict';
class PresetSource {
constructor(name, url, gitHubBranch = "") {
this.name = name;
this.url = url;
this.gitHubBranch = gitHubBranch;
this.official = false;
}
static isUrlGithubRepo(url) {
return url.trim().toLowerCase().startsWith("https://github.com/");
}
}

View file

@ -0,0 +1,70 @@
.presets_source_panel {
background-color: var(--boxBackground);
border: 1px solid var(--subtleAccent);
padding: 1.5ex;
box-shadow: 2px 2px 5px rgba(92, 92, 92, 0.25);
border-radius: 4px;
margin-bottom: 6px;
}
.presets_source_panel_not_selected {
cursor: pointer;
}
.presets_source_panel_selected {
cursor: unset;
}
.presets_source_panel_not_selected:hover {
background-color: var(--subtleAccent);
box-shadow: 2px 2px 5px rgba(92, 92, 92, 0.5);
}
.presets_source_panel_editing_table {
display: table;
width: 100%;
border-spacing: 6px;
}
.presets_source_panel_editing_row {
display: table-row;
}
.presets_source_panel_editing_field_label {
display: table-cell;
white-space: pre;
padding-right: 10px;
min-width: 100px;
}
.presets_source_panel_editing_field_edit {
display: table-cell;
width: 100%;
padding-right: 10px;
}
.presets_source_panel_editing_row .standard_input {
width: 100%;
margin-right: 12px;
}
.presets_source_panel_no_editing_name {
font-size: 130%;
display: inline-block;
vertical-align: middle;
}
.presets_source_panel_no_editing_selected {
background-image: url(../../../images/icons/cf_icon_check_orange.svg);
width: 30px;
height: 30px;
display: inline-block;
margin-top: 8px;
margin-bottom: 8px;
margin-right: 5px;
vertical-align: middle;
}
.presets_source_panel_reset, .presets_source_panel_save, .presets_source_panel_delete {
float: right;
}

View file

@ -0,0 +1,29 @@
<div class="presets_source_panel">
<div class="presets_source_panel_no_editing">
<div class="presets_source_panel_no_editing_selected"></div>
<div class="presets_source_panel_no_editing_name"></div>
</div>
<div class="presets_source_panel_editing">
<div class="presets_source_panel_editing_table">
<div class="presets_source_panel_editing_row">
<div class="presets_source_panel_editing_field_label">Name</div>
<div class="presets_source_panel_editing_field_edit"><input type="text" class="presets_source_panel_editing_name_field standard_input"/></div>
</div>
<div class="presets_source_panel_editing_row">
<div class="presets_source_panel_editing_field_label">Url</div>
<div class="presets_source_panel_editing_field_edit"><input type="text" class="presets_source_panel_editing_url_field standard_input"/></div>
</div>
<div class="presets_source_panel_editing_row presets_source_panel_editing_github_branch">
<div class="presets_source_panel_editing_field_label">GitHub branch</div>
<div class="presets_source_panel_editing_field_edit"><input type="text" class="presets_source_panel_editing_branch_field standard_input"/></div>
</div>
</div>
<div class="btn">
<div class="presets_source_panel_no_editing_selected"></div>
<a href="#" class="tool regular-button presets_source_panel_activate" i18n="presetsSourcesDialogMakeSourceActive"></a>
<a href="#" class="tool regular-button presets_source_panel_save" i18n="presetsSourcesDialogSaveSource"></a>
<a href="#" class="tool regular-button presets_source_panel_reset" i18n="presetsSourcesDialogResetSource"></a>
<a href="#" class="tool regular-button presets_source_panel_delete" i18n="presetsSourcesDialogDeleteSource"></a>
</div>
</div>
</div>

View file

@ -0,0 +1,199 @@
'use strict';
class SourcePanel {
constructor(parentDiv, presetSource) {
this._parentDiv = parentDiv;
this._presetSource = presetSource;
this._active = false;
}
get presetSource() {
return this._presetSource;
}
load() {
SourcePanel.s_panelCounter++;
this._domId = `source_panel_${SourcePanel.s_panelCounter}`;
this._parentDiv.append(`<div id="${this._domId}"></div>`);
this._domWrapperDiv = $(`#${this._domId}`);
this._domWrapperDiv.toggle(false);
return new Promise(resolve => {
this._domWrapperDiv.load("./tabs/presets/SourcesDialog/SourcePanel.html",
() => {
this._setupHtml();
resolve();
});
});
}
setOnSelectedCallback(onSelectedCallback) {
// callback with this (SourcePanel) argument
// so that consumer knew which panel was clicked on
this._onSelectedCallback = onSelectedCallback;
}
setOnDeleteCallback(onDeletedCallback) {
// callback with this (SourcePanel) argument
// so that consumer knew which panel was clicked on
this._onDeletedCallback = onDeletedCallback;
}
setOnActivateCallback(onActivateCallback) {
// callback with this (SourcePanel) argument
// so that consumer knew which panel was clicked on
this._onActivateCallback = onActivateCallback;
}
setOnSaveCallback(onSaveCallback) {
// callback with this (SourcePanel) argument
// so that consumer knew which panel was clicked on
this._onSaveCallback = onSaveCallback;
}
setSelected(isSelected) {
this._setUiSelected(isSelected);
}
get active() {
return this._active;
}
setActive(isActive) {
this._active = isActive;
this._domDivSelectedIndicator.toggle(this._active);
this._domButtonActivate.toggle(!isActive);
}
_setUiOfficial() {
if (this.presetSource.official){
this._domButtonSave.toggle(false);
this._domButtonReset.toggle(false);
this._domButtonDelete.toggle(false);
this._domEditName.prop("disabled", true);
this._domEditUrl.prop("disabled", true);
this._domEditGitHubBranch.prop("disabled", true);
}
}
_setUiSelected(isSelected) {
if (this._selected !== isSelected) {
this._domDivNoEditing.toggle(!isSelected);
this._domDivEditing.toggle(isSelected);
this._onResetButtonClick();
this._updateNoEditingName();
this._domDivInnerPanel.toggleClass("presets_source_panel_not_selected", !isSelected);
this._domDivInnerPanel.toggleClass("presets_source_panel_selected", isSelected);
if (isSelected) {
this._domDivInnerPanel.off("click");
} else {
this._domDivInnerPanel.on("click", () => this._onPanelSelected());
}
this._selected = isSelected;
}
}
_updateNoEditingName() {
this._domDivNoEditingName.text(this._presetSource.name);
}
_setupHtml() {
this._readDom();
this._setupActions();
this.setSelected(false);
this._setIsSaved(true);
this._checkIfGithub();
this.setActive(this._active);
this._setUiOfficial();
i18n.localizePage();
this._domWrapperDiv.toggle(true);
}
_setupActions() {
this._domButtonSave.on("click", () => this._onSaveButtonClick());
this._domButtonReset.on("click", () => this._onResetButtonClick());
this._domButtonDelete.on("click", () => this._onDeleteButtonClick());
this._domButtonActivate.on("click", () => this._onActivateButtonClick());
this._domEditName.on("input", () => this._onInputChange());
this._domEditUrl.on("input", () => this._onInputChange());
this._domEditGitHubBranch.on("input", () => this._onInputChange());
}
_onPanelSelected() {
this._setUiSelected(true);
this._onSelectedCallback?.(this);
}
_checkIfGithub() {
const isGithubUrl = PresetSource.isUrlGithubRepo(this._domEditUrl.val());
this._domDivGithubBranch.toggle(isGithubUrl);
}
_onInputChange() {
this._checkIfGithub();
this._setIsSaved(false);
}
_onSaveButtonClick() {
this._presetSource.name = this._domEditName.val();
this._presetSource.url = this._domEditUrl.val();
this._presetSource.gitHubBranch = this._domEditGitHubBranch.val();
this._setIsSaved(true);
this._onSaveCallback?.(this);
}
_onResetButtonClick() {
this._domEditName.val(this._presetSource.name);
this._domEditUrl.val(this._presetSource.url);
this._domEditGitHubBranch.val(this._presetSource.gitHubBranch);
this._checkIfGithub();
this._setIsSaved(true);
}
_onDeleteButtonClick() {
this._domWrapperDiv.remove();
this._onDeletedCallback?.(this);
}
_onActivateButtonClick() {
this.setActive(true);
this._onActivateCallback?.(this);
}
_setIsSaved(isSaved) {
if (isSaved) {
this._domButtonSave.addClass(GUI.buttonDisabledClass);
this._domButtonReset.addClass(GUI.buttonDisabledClass);
} else {
this._domButtonSave.removeClass(GUI.buttonDisabledClass);
this._domButtonReset.removeClass(GUI.buttonDisabledClass);
}
}
_readDom() {
this._domDivInnerPanel = this._domWrapperDiv.find(".presets_source_panel");
this._domDivNoEditing = this._domWrapperDiv.find(".presets_source_panel_no_editing");
this._domDivEditing = this._domWrapperDiv.find(".presets_source_panel_editing");
this._domEditName = this._domWrapperDiv.find(".presets_source_panel_editing_name_field");
this._domEditUrl = this._domWrapperDiv.find(".presets_source_panel_editing_url_field");
this._domEditGitHubBranch = this._domWrapperDiv.find(".presets_source_panel_editing_branch_field");
this._domButtonSave = this._domWrapperDiv.find(".presets_source_panel_save");
this._domButtonReset = this._domWrapperDiv.find(".presets_source_panel_reset");
this._domButtonActivate = this._domWrapperDiv.find(".presets_source_panel_activate");
this._domButtonDelete = this._domWrapperDiv.find(".presets_source_panel_delete");
this._domDivGithubBranch = this._domWrapperDiv.find(".presets_source_panel_editing_github_branch");
this._domDivNoEditingName = this._domWrapperDiv.find(".presets_source_panel_no_editing_name");
this._domDivSelectedIndicator = this._domWrapperDiv.find(".presets_source_panel_no_editing_selected");
}
}
SourcePanel.s_panelCounter = 0;

View file

@ -0,0 +1,27 @@
.presets_sources_dialog_title_panel {
padding-bottom: .5ex;
border-bottom: 1px solid var(--accent);
margin-bottom: 2ex;
font-size: 1.5em;
font-weight: bold;
}
.presets_sources_dialog_scrollable {
height: 430px;
overflow-y: auto;
overflow-x: hidden;
}
@media all and (max-width: 575px) {
#presets_sources_dialog_content_wrapper .content_toolbar {
position: fixed;
}
.presets_sources_dialog_scrollable {
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 51px;
height: unset;
}
}

View file

@ -0,0 +1,16 @@
<div id="presets_sources_dialog_content_wrapper">
<div id="presets_sources_dialog_content">
<div class="presets_sources_dialog_title_panel" i18n="presetsSourcesDialogTitle"></div>
<div class="presets_sources_dialog_scrollable">
<div class="note" i18n="presets_sources_dialog_warning"></div>
<div class="presets_sources_dialog_sources"></div>
</div>
</div>
<div class="content_toolbar">
<div class="btn">
<a id="presets_sources_dialog_close" i18n="OK" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
<a id="presets_sources_dialog_add_new" i18n="presetsSourcesDialogAddNew" class="tool regular-button" target="_blank" rel="noopener noreferrer"></a>
</div>
</div>
</div>

View file

@ -0,0 +1,193 @@
'use strict';
class PresetsSourcesDialog {
constructor(domDialog) {
this._domDialog = domDialog;
this._sourceSelectedPromiseResolve = null;
this._sourcesPanels = [];
this._sources = [];
this._activeSourceIndex = 0;
}
load() {
return new Promise(resolve => {
this._domDialog.load("./tabs/presets/SourcesDialog/SourcesDialog.html",
() => {
this._setupDialog();
this._initializeSources();
resolve();
});
});
}
show() {
this._domDialog[0].showModal();
return new Promise(resolve => this._sourceSelectedPromiseResolve = resolve);
}
getActivePresetSource() {
return this._sources[this._activeSourceIndex];
}
get isOfficialActive() {
return this._activeSourceIndex === 0;
}
_initializeSources() {
this._sources = this._readSourcesFromStorage();
this._activeSourceIndex = this._readActiveSourceIndexFromStorage(this._sources.length);
for (let i = 0; i < this._sources.length; i++) {
const isActive = this._activeSourceIndex === i;
this._addNewSourcePanel(this._sources[i], isActive, false);
}
}
_readSourcesFromStorage() {
const officialSource = this._createOfficialSource();
const officialSourceSecondary = this._createSecondaryOfficialSource();
const obj = ConfigStorage.get('PresetSources');
let sources = obj.PresetSources;
if (sources && sources.length > 0) {
sources[0] = officialSource;
} else {
sources = [officialSource, officialSourceSecondary];
}
if (sources.length === 1) {
sources.push(officialSourceSecondary);
}
return sources;
}
_readActiveSourceIndexFromStorage(sourcesCount) {
const obj = ConfigStorage.get('PresetSourcesActiveIndex');
const index = Number(obj.PresetSourcesActiveIndex);
let result = 0;
if (index && Number.isInteger(index) && index < sourcesCount) {
result = index;
}
return result;
}
_createOfficialSource() {
const officialSource = new PresetSource("Betaflight Official Presets", "https://api.betaflight.com/firmware-presets/", "");
officialSource.official = true;
return officialSource;
}
_createSecondaryOfficialSource() {
const officialSource = new PresetSource("Betaflight Presets - GitHub BACKUP", "https://github.com/betaflight/firmware-presets", "backup");
officialSource.official = false;
return officialSource;
}
_setupDialog() {
this._readDom();
this._setupEvents();
this._domButtonAddNew.on("click", () => this._onAddNewSourceButtonClick());
i18n.localizePage();
}
_onAddNewSourceButtonClick() {
const presetSource = new PresetSource(i18n.getMessage("presetsSourcesDialogDefaultSourceName"), "", "");
this._addNewSourcePanel(presetSource).then(() => {
this._scrollDown();
this._updateSourcesFromPanels();
});
}
_scrollDown() {
this._domDivSourcesPanel.stop();
this._domDivSourcesPanel.animate({scrollTop: this._domDivSourcesPanel.prop('scrollHeight') + "px"});
}
_addNewSourcePanel(presetSource, isActive = false, isSelected = true) {
const sourcePanel = new SourcePanel(this._domDivSourcesPanel, presetSource);
this._sourcesPanels.push(sourcePanel);
return sourcePanel.load().then(() => {
sourcePanel.setOnSelectedCallback(selectedPanel => this._onSourcePanelSelected(selectedPanel));
sourcePanel.setOnDeleteCallback(selectedPanel => this._onSourcePanelDeleted(selectedPanel));
sourcePanel.setOnActivateCallback(selectedPanel => this._onSourcePanelActivated(selectedPanel));
sourcePanel.setOnSaveCallback(() => this._onSourcePanelSaved());
sourcePanel.setActive(isActive);
if (isSelected) {
this._onSourcePanelSelected(sourcePanel);
}
});
}
_setupEvents() {
this._domButtonClose.on("click", () => this._onCloseButtonClick());
}
_onCloseButtonClick() {
this._sourceSelectedPromiseResolve?.();
this._domDialog[0].close();
}
_readPanels() {
this._sources = [];
this._activeSourceIndex = 0;
for(let i = 0; i < this._sourcesPanels.length; i++) {
this._sources.push(this._sourcesPanels[i].presetSource);
if (this._sourcesPanels[i].active) {
this._activeSourceIndex = i;
}
}
}
_saveSources() {
ConfigStorage.set({'PresetSources': this._sources});
ConfigStorage.set({'PresetSourcesActiveIndex': this._activeSourceIndex});
}
_updateSourcesFromPanels() {
this._readPanels();
this._saveSources();
}
_onSourcePanelSaved() {
this._updateSourcesFromPanels();
}
_onSourcePanelSelected(selectedPanel) {
for (const panel of this._sourcesPanels) {
if (panel !== selectedPanel) {
panel.setSelected(false);
} else {
panel.setSelected(true);
}
}
}
_onSourcePanelDeleted(selectedPanel) {
this._sourcesPanels = this._sourcesPanels.filter(panel => panel !== selectedPanel);
if (selectedPanel.active) {
this._sourcesPanels[0].setActive(true);
}
this._updateSourcesFromPanels();
}
_onSourcePanelActivated(selectedPanel) {
for (const panel of this._sourcesPanels) {
if (panel !== selectedPanel) {
panel.setActive(false);
} else {
panel.setActive(true);
}
}
this._updateSourcesFromPanels();
}
_readDom() {
this._domButtonAddNew = $("#presets_sources_dialog_add_new");
this._domButtonClose = $("#presets_sources_dialog_close");
this._domDivSourcesPanel = $(".presets_sources_dialog_sources");
}
}

View file

@ -0,0 +1,82 @@
.preset_title_panel {
color: var(--defaultText);
}
.preset_title_panel_border {
background-color: var(--boxBackground);
border: 1px solid var(--subtleAccent);
cursor: pointer;
padding: 1.5ex;
box-shadow: 2px 2px 5px rgba(92, 92, 92, 0.25);
border-radius: 4px;
}
.preset_title_panel_title {
font-size: 1.5em;
font-weight: bold;
display: block;
margin-bottom: 1ex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.preset_title_panel_category {
color: var(--mutedText);
font-weight: bold;
}
.preset_title_panel_official {
padding-left: 3px;
padding-right: 3px;
padding-top: 3px;
padding-bottom: 3px;
display: inline-block;
color: white;
font-weight: 700;
border-radius: 4px;
}
.preset_title_panel_status_official {
background-color: green;
}
.preset_title_panel_status_community {
background-color: var(--accent);
}
.preset_title_panel_status_experimental {
background-color: red;
}
.preset_title_panel_versions_text {
font-weight: bold;
}
.preset_title_panel_keywords_text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.presets_title_panel_table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.presets_title_panel_table td{
overflow: hidden;
height: 24px;
width: auto;
text-overflow: ellipsis;
white-space: nowrap;
}
.presets_title_panel_table td:nth-child(1){
width: 100px;
}
.preset_title_panel_label {
color: var(--quietHeader);
}

View file

@ -0,0 +1,92 @@
'use strict';
class PresetTitlePanel
{
constructor(parentDiv, preset, clickable, onLoadedCallback)
{
PresetTitlePanel.s_panelCounter ++;
this._parentDiv = parentDiv;
this._onLoadedCallback = onLoadedCallback;
this._domId = `preset_title_panel_${PresetTitlePanel.s_panelCounter}`;
this._preset = preset;
this._parentDiv.append(`<div class="${this._domId}"></div>`);
this._domWrapperDiv = $(`.${this._domId}`);
this._domWrapperDiv.toggle(false);
if (clickable) {
this._domWrapperDiv.addClass("preset_title_panel_border");
// setting up hover effect here, because if setup in SCC it stops working after background animation like - this._domWrapperDiv.animate({ backgroundColor....
this._domWrapperDiv.on("mouseenter", () => this._domWrapperDiv.css({"background-color": "var(--subtleAccent)"}));
this._domWrapperDiv.on("mouseleave", () => this._domWrapperDiv.css({"background-color": "var(--boxBackground)"}));
}
}
load() {
this._domWrapperDiv.load("./tabs/presets/TitlePanel/PresetTitlePanelBody.html", () => this._setupHtml());
}
subscribeClick(presetsDetailedDialog, presetsRepo)
{
this._domWrapperDiv.on("click", () => {
presetsDetailedDialog.open(this._preset, presetsRepo).then(isPresetPicked => {
if (isPresetPicked) {
const color = this._domWrapperDiv.css( "background-color" );
this._domWrapperDiv.css('background-color', 'green');
this._domWrapperDiv.animate({ backgroundColor: color }, 2000);
this.setPicked(true);
}
});
});
}
setPicked(isPicked) {
this._preset.isPicked = isPicked;
if (isPicked) {
this._domWrapperDiv.css({"border": "2px solid green"});
} else {
this._domWrapperDiv.css({"border": "1px solid var(--subtleAccent)"});
}
}
_setupHtml()
{
this._readDom();
this._domCategory.text(this._preset.category);
this._domTitle.text(this._preset.title);
this._domTitle.prop("title", this._preset.title);
this._domAuthor.text(this._preset.author);
this._domVersions.text(this._preset.firmware_version?.join("; "));
this._domKeywords.text(this._preset.keywords?.join("; "));
this._domKeywords.prop("title", this._preset.keywords?.join("; "));
this._domStatusOfficial.toggle(this._preset.status === "OFFICIAL");
this._domStatusCommunity.toggle(this._preset.status === "COMMUNITY");
this._domStatusExperimental.toggle(this._preset.status === "EXPERIMENTAL");
this.setPicked(this._preset.isPicked);
i18n.localizePage();
this._domWrapperDiv.toggle(true);
this._onLoadedCallback?.();
}
_readDom()
{
this._domTitle = this._domWrapperDiv.find('.preset_title_panel_title');
this._domCategory = this._domWrapperDiv.find('.preset_title_panel_category');
this._domAuthor = this._domWrapperDiv.find('.preset_title_panel_author_text');
this._domKeywords = this._domWrapperDiv.find('.preset_title_panel_keywords_text');
this._domVersions = this._domWrapperDiv.find('.preset_title_panel_versions_text');
this._domStatusOfficial = this._domWrapperDiv.find('.preset_title_panel_status_official');
this._domStatusCommunity = this._domWrapperDiv.find('.preset_title_panel_status_community');
this._domStatusExperimental = this._domWrapperDiv.find('.preset_title_panel_status_experimental');
}
remove()
{
this._domWrapperDiv.remove();
}
}
PresetTitlePanel.s_panelCounter = 0;

View file

@ -0,0 +1,45 @@
<div class="preset_title_panel">
<div>
<span class="preset_title_panel_title"></span>
</div>
<div>
<table class="presets_title_panel_table" role="presentation">
<tbody>
<tr>
<td>
<span class="preset_title_panel_official preset_title_panel_status_experimental" i18n="presetsExperimental"></span>
<span class="preset_title_panel_official preset_title_panel_status_community" i18n="presetsCommunity"></span>
<span class="preset_title_panel_official preset_title_panel_status_official" i18n="presetsOfficial"></span>
</td>
<td>
<span class="preset_title_panel_category"></span>
</td>
</tr>
<tr>
<td>
<span class="preset_title_panel_label preset_title_panel_author_label" i18n="presetsAuthor"></span>
</td>
<td>
<span class="preset_title_panel_author_text"></span>
</td>
</tr>
<tr>
<td>
<span class="preset_title_panel_label preset_title_panel_versions_label" i18n="presetsVersions"></span>
</td>
<td>
<span class="preset_title_panel_versions_text"></span>
</td>
</tr>
<tr>
<td>
<span class="preset_title_panel_label preset_title_panel_keywords_label" i18n="presetsKeywords"></span>
</td>
<td>
<span class="preset_title_panel_keywords_text"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,426 @@
.tab-presets {
height: 100%;
}
.tab-presets .content_wrapper {
height: calc(100% - 30px - 3ex);
overflow-y: scroll;
overflow-x: hidden;
}
.tab-presets p {
padding: 0;
border: 0 dotted var(--subtleAccent);
}
.tab-presets .presets_warnings {
padding-left : 20px;
padding-right : 20px;
}
#presets_content_wrapper {
padding: 0px;
position: relative;
}
.presets_top_bar_pannel {
margin-left: auto;
}
.presets_title_text {
display: inline-block;
}
.presets_top_bar_button_pannel {
display: inline;
}
.presets_warning_backup {
display: grid;
grid-template-columns: 1fr fit-content(300px);
}
.presets_warning_backup_text {
padding-right: 24px;
}
.presets_warning_backup_button_hide {
margin-top: 0px;
margin-bottom: 0px;
margin-right: 0px;
line-height: 17px;
font-size: 10px;
height: 17px;
}
.tab-presets .top_panel_spacer {
width: 0px;
display: inline;
border: 1px var(--subtleAccent);
border-style: none none none solid;
height: 60%;
margin-right: 10px;
float: right;
}
.tab-presets .tab_title .presets_top_bar_button_pannel .regular-button{
margin-bottom: 0px;
margin-top: 0px;
line-height : 17px;
font-size : 10px;
border-radius: 3px;
}
.presetsWikiButton {
margin-right: 0px;
}
.tab-presets .tab_title
{
padding: 20px 20px 0px 20px;
}
#presets_list {
padding: 0px 0px 20px 0px;
height: calc(100% - 180px);
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
}
#preset_list_wrapper {
padding-left: 20px;
padding-right: 20px;
}
#presets_global_loading {
padding: 0px 20px 20px 20px;
display: none;
width: 50%;
height: 50%;
margin: auto;
}
#presets_global_loading_error {
padding: 0px 20px 20px 20px;
display: none;
}
#presets_main_content {
display: none;
}
#presets_list:last-child::after {
visibility: hidden;
display: block;
font-size: 0;
content: "&nbsp;";
clear: both;
height: 0;
}
.presets_filter_select {
width: 100%;
background-color: var(--boxBackground);
color: var(--defaultText);
}
.presets_search_settings {
position: sticky;
top: 0px;
background-color: var(--boxBackground);
}
.tab-presets .ms-choice,.ms-drop {
background-color: var(--boxBackground)!important;
color: var(--defaultText)!important;
}
.presets_text_input {
border: 1px solid var(--subtleAccent);
border-radius: 3px;
background-color: var(--boxBackground);
color: var(--defaultText);
}
#presets_filter_text {
height: 26px;
flex: 1;
padding-left: 5px;
}
#presets_search_hint {
float: left;
width: 28px;
height: 28px;
margin-right: 12px;
background-repeat: no-repeat;
background-image: url(../../images/icons/cf_icon_search_orange.svg);
}
#presets_search_bar_wrapper {
display: flex;
padding: 2ex 20px 2ex 20px;
}
#presets_cli {
width: 100%;
}
#presets_cli_background {
border: 1px solid var(--subtleAccent);
background-color: rgba(64, 64, 64, 1);
margin-top: 0;
height: 300px;
border-radius: 5px;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.80);
}
.tab-presets .window {
height: 100%;
width: 100%;
padding: 5px;
overflow-y: auto;
overflow-x: hidden;
font-family: monospace;
color: white;
box-sizing: border-box;
-webkit-user-select: text;
user-select: text;
float: left;
}
.presets_fitler_table_header
{
background: var(--quietHeader);
color: var(--quietText);
border-right: 1px solid var(--subtleAccent);
font-weight: normal;
text-align: left;
padding: 4px 4px 4px 6px;
}
.presets_fitler_table_value {
background-color: var(--alternativeBackground);
width: 100%;
}
.tab-presets textarea[name='commands'] {
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
margin-top: 6px;
padding: 4px;
padding-left: 8px;
padding-right: 8px;
color: white;
border: 1px solid var(--subtleAccent);
background-color: rgba(64, 64, 64, 1);
resize: none;
}
.tab-presets .presets_cli_errors_dialog_warning {
font-size: 1.2em;
margin-bottom: 8px;
}
.tab-presets #content-watermark {
z-index: 0;
}
#presets_cli_errors_dialog .regular-button {
margin-bottom: 0px;
margin-left: 12px;
margin-right: 0px;
float: right;
}
.tab-presets .window .wrapper {
white-space: pre-wrap;
user-select: text;
}
.tab-presets .window .error_message {
color: red;
font-weight: bold;
}
.tab-presets .save {
color: white;
}
#presets_detailed_dialog {
width : 600px;
height : 520px;
padding: 12px;
}
#presets_sources_dialog {
width : 600px;
height : 520px;
padding: 12px;
}
#presets_cli_errors_dialog {
width : 600px;
padding: 24px;
}
#presets_apply_progress_dialog {
width: 300px;
padding: 24px;
}
.tab-presets .presets_apply_progress_dialog_progress_bar {
width: 100%;
height: 20px;
margin-top: 12px;
}
.tab-presets .ms-drop {
z-index: 2001;
}
/* 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"] {
margin-top: 0.1rem !important;
}
.presets_filter_table_wrapper {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
padding: 0px 20px 0px 20px;
}
.presets_filter_table_wrapper .freelabel {
display: inline-block;
padding: 4px 0 4px 0;
}
.tab-presets .cf_doc_version_bt {
margin-top: 20px;
}
#presets_list_no_found, #presets_list_too_many_found {
font-size: 1.5em;
}
@media only screen and (max-width: 1055px) , only screen and (max-device-width: 1055px) {
.tab-presets .content_wrapper {
height: calc(100% - 87px);
}
.tab-presets .content_toolbar {
margin-top: 5px;
}
.presets_search_settings {
position: static;
top: unset;
}
}
.presets_secondary_top_bar_panel_buttons {
padding-left: 8px;
padding-right: 8px;
margin-bottom: 8px;
}
.presets_secondary_top_bar_panel_buttons .regular-button {
margin-top: 0px;
margin-bottom: 0px;
line-height: 17px;
font-size: 10px;
border-radius: 3px;
}
@media all and (max-width: 575px) {
.tab-presets .content_wrapper {
height: calc(100% - 51px);
}
.tab-presets .tab_title
{
padding: 20px 10px 10px 10px;
}
.tab-presets .backdrop {
background-size: 100%;
}
#presets_list {
grid-template-columns: 100%;
}
#preset_list_wrapper, .presets_search_settings {
padding-left: 8px;
padding-right: 8px;
}
#presets_search_bar_wrapper {
padding-left: 0px;
padding-right: 0px;
padding-top: 1ex;
}
.presets_filter_row {
display: table-row;
}
.presets_filter_table_wrapper {
display: table;
border-spacing: 6px;
margin-right: -6px;
margin-left: -6px;
padding-left: 0px;
padding-right: 0px;
}
.presets_fitler_table_header {
display: table-cell;
background-color: unset;
border-right: unset;
}
.presets_fitler_table_value {
display: table-cell;
}
.tab-presets .presets_warnings {
padding-left: 8px;
padding-right: 8px;
}
#presets_cli_errors_dialog {
padding: 12px;
}
#presets_apply_progress_dialog {
padding: 12px;
}
#presets_cli_errors_dialog .btn {
position: fixed;
right: 12px;
bottom: 12px;
}
#presets_cli {
height: calc(100% - 121px);
}
#presets_cli_background {
height: 100%;
}
.presets_warning_backup {
display: block;
grid-template-columns: 1fr fit-content(300px);
}
.presets_warning_backup_text {
padding-right: 24px;
margin-bottom: 6px;
}
}

View file

@ -0,0 +1,116 @@
<div class="tab-presets toolbar_fixed_bottom">
<div class="content_wrapper" id="presets_content_wrapper">
<div class="tab_title">
<div i18n="tabPresets" class="presets_title_text"></div>
<div class="presets_top_bar_button_pannel">
<a i18n="presetsWiki" class="presetsWikiButton tool regular-button right" href="https://github.com/betaflight/betaflight/wiki/Presets-in-BF-4.3" target="_blank" rel="noopener noreferrer">Presets Wiki</a>
<div class = "top_panel_spacer visible-on-desktop-only"></div>
<a href="#" class="presets_sources_show tool regular-button right" i18n="presetSources"></a>
<div class = "top_panel_spacer visible-on-desktop-only"></div>
<a href="#" class="presets_load_config visible-on-desktop-only tool regular-button right" i18n="presetsBackupLoad"></a>
<a href="#" class="presets_save_config visible-on-desktop-only tool regular-button right" i18n="presetsBackupSave"></a>
</div>
</div>
<div class="visible-on-phone-only presets_secondary_top_bar_panel_buttons">
<a href="#" class="presets_load_config tool regular-button disabled" i18n="presetsBackupLoad"></a>
<a href="#" class="presets_save_config tool regular-button disabled" i18n="presetsBackupSave"></a>
</div>
<div class="presets_warnings">
<div class="note presets_warning_not_official_source" i18n="presetsWarningNotOfficialSource"></div>
<div class="note presets_warning_backup">
<div class="presets_warning_backup_text" i18n="presetsWarningBackup"></div>
<a href="#" id="" class="tool regular-button presets_warning_backup_button_hide" i18n="dontShowAgain">Don't show again</a>
</div>
</div>
<div id="presets_global_loading" class="data-loading">
</div>
<div id="presets_global_loading_error">
<h3 i18n="presetsLoadError"></h3>
<a href="#" id="presets_reload" class="tool regular-button" i18n="presetsReload"></a>
</div>
<div id="presets_main_content">
<div class="presets_search_settings">
<div class="presets_filter_table_wrapper">
<div class="presets_filter_row">
<div class = "presets_fitler_table_header" i18n="presetsFilterCategory"></div>
<div class = "presets_fitler_table_value">
<select multiple="multiple" class="presets_filter_select" id="presets_filter_category"></select>
</div>
</div>
<div class="presets_filter_row">
<div class = "presets_fitler_table_header" i18n="presetsFilterKeyword"></div>
<div class = "presets_fitler_table_value">
<select multiple="multiple" class="presets_filter_select" id="presets_filter_keyword"></select>
</div>
</div>
<div class="presets_filter_row">
<div class = "presets_fitler_table_header" i18n="presetsFilterAuthor"></div>
<div class = "presets_fitler_table_value">
<select multiple="multiple" class="presets_filter_select" id="presets_filter_author"></select>
</div>
</div>
<div class="presets_filter_row">
<div class = "presets_fitler_table_header" i18n="presetsFilterFirmware"></div>
<div class = "presets_fitler_table_value">
<select multiple="multiple" class="presets_filter_select" id="presets_filter_firmware_version"></select>
</div>
</div>
<div class="presets_filter_row">
<div class = "presets_fitler_table_header" i18n="presetsFilterStatus"></div>
<div class = "presets_fitler_table_value">
<select multiple="multiple" class="presets_filter_select" id="presets_filter_status"></select>
</div>
</div>
</div>
<div id="presets_search_bar_wrapper">
<div id="presets_search_hint">
</div>
<input type="text" class="presets_text_input" id="presets_filter_text"/>
</div>
</div>
<div id="preset_list_wrapper">
<div id="presets_list_no_found" i18n="presetsNoPresetsFound"></div>
<div id="presets_list"></div>
<div id="presets_list_too_many_found" i18n="presetsTooManyPresetsFound">Too many found</div>
</div>
</div>
</div>
<div class="content_toolbar">
<div class="btn">
<a href="#" id="presets_save_button" class="tool regular-button" i18n="presetsButtonSave"></a>
<a href="#" id="presets_cancel_button" class="tool regular-button" i18n="presetsButtonCancel"></a>
</div>
</div>
<dialog id="presets_detailed_dialog">
</dialog>
<dialog id="presets_sources_dialog">
</dialog>
<dialog id="presets_apply_progress_dialog">
<div class="presets_apply_progress_dialog_label" i18n="presetsApplyingPresets"></div>
<div class="presets_apply_progress_dialog_please_wait" i18n="presetsPleaseWait"></div>
<progress class="presets_apply_progress_dialog_progress_bar" value="0" max="100"></progress>
</dialog>
<dialog id="presets_cli_errors_dialog">
<div class="presets_cli_errors_dialog_warning" i18n="presetsCliErrorsWaring"></div>
<div id="presets_cli">
<div id="presets_cli_background">
<div class="window" id="presets_cli_window">
<div class="wrapper" id="presets_cli_window_wrapper"></div>
</div>
</div>
<div class="commandline">
<textarea name="commands" id="presets_cli_command" rows="1" cols="0"></textarea>
</div>
</div>
<div class="btn">
<a href="#" id="presets_cli_errors_save_anyway_button" class="tool regular-button" i18n="presetsSaveAnyway"></a>
<a href="#" id="presets_cli_errors_exit_no_save_button" class="tool regular-button" i18n="presetsButtonCancel"></a>
</div>
</dialog>
</div>

549
src/tabs/presets/presets.js Normal file
View file

@ -0,0 +1,549 @@
'use strict';
TABS.presets = {
presetsRepo: null,
cliEngine: null,
pickedPresetList: [],
};
TABS.presets.initialize = function (callback) {
const self = this;
self.cliEngine = new CliEngine(self);
self.cliEngine.setProgressCallback(value => this.onApplyProgressChange(value));
self._presetPanels = [];
$('#content').load("./tabs/presets/presets.html", () => self.onHtmlLoad(callback));
if (GUI.active_tab !== 'presets') {
GUI.active_tab = 'presets';
}
};
TABS.presets.readDom = function() {
this._divGlobalLoading = $('#presets_global_loading');
this._divGlobalLoadingError = $('#presets_global_loading_error');
this._divCli = $('#presets_cli');
this._divMainContent = $('#presets_main_content');
this._selectCategory = $('#presets_filter_category');
this._selectKeyword = $('#presets_filter_keyword');
this._selectAuthor = $('#presets_filter_author');
this._selectFirmwareVersion = $('#presets_filter_firmware_version');
this._selectStatus = $('#presets_filter_status');
this._inputTextFilter = $('#presets_filter_text');
this._divPresetList = $('#presets_list');
this._domButtonSave = $("#presets_save_button");
this._domButtonCancel = $("#presets_cancel_button");
this._domReloadButton = $("#presets_reload");
this._domContentWrapper = $("#presets_content_wrapper");
this._domProgressDialog = $("#presets_apply_progress_dialog")[0];
this._domProgressDialogProgressBar = $(".presets_apply_progress_dialog_progress_bar");
this._domButtonaSaveAnyway = $("#presets_cli_errors_save_anyway_button");
this._domButtonaCliExit = $("#presets_cli_errors_exit_no_save_button");
this._domDialogCliErrors = $("#presets_cli_errors_dialog")[0];
this._domButtonSaveBackup = $(".presets_save_config");
this._domButtonLoadBackup = $(".presets_load_config");
this._domButtonPresetSources = $(".presets_sources_show");
this._domWarningNotOfficialSource = $(".presets_warning_not_official_source");
this._domWarningBackup = $(".presets_warning_backup");
this._domButtonHideBackupWarning = $(".presets_warning_backup_button_hide");
this._domListNoFound = $("#presets_list_no_found");
this._domListTooManyFound = $("#presets_list_too_many_found");
};
TABS.presets.getPickedPresetsCli = function() {
let result = [];
this.pickedPresetList.forEach(pickedPreset => {
result.push(...pickedPreset.presetCli);
});
result = result.filter(command => command !== "");
return result;
};
TABS.presets.onApplyProgressChange = function(value) {
this._domProgressDialogProgressBar.val(value);
};
TABS.presets.applyCommandsList = function(strings) {
strings.forEach(cliCommand => {
this.cliEngine.sendLine(cliCommand);
});
};
TABS.presets.onSaveClick = function() {
this._domProgressDialogProgressBar.val(0);
this._domProgressDialog.showModal();
const currentCliErrorsCount = this.cliEngine.errorsCount;
this.activateCli().then(() => {
const cliCommandsArray = this.getPickedPresetsCli();
this.cliEngine.executeCommandsArray(cliCommandsArray).then(() => {
const newCliErrorsCount = this.cliEngine.errorsCount;
if (newCliErrorsCount !== currentCliErrorsCount) {
this._domProgressDialog.close();
this._domDialogCliErrors.showModal();
} else {
this._domProgressDialog.close();
this.cliEngine.sendLine(CliEngine.s_commandSave);
this.disconnectCliMakeSure();
}
});
});
};
TABS.presets.disconnectCliMakeSure = function() {
GUI.timeout_add('disconnect', function () {
$('div.connect_controls a.connect').trigger( "click" );
}, 500);
};
TABS.presets.setupMenuButtons = function() {
this._domButtonSave.on("click", () => this.onSaveClick());
this._domButtonCancel.on("click", () => {
for(const pickedPreset of this.pickedPresetList) {
pickedPreset.preset.isPicked = false;
}
this.updateSearchResults();
this.pickedPresetList.length = 0;
this.enableSaveCancelButtons(false);
});
this._domButtonaCliExit.on("click", () =>{
this._domDialogCliErrors.close();
this.cliEngine.sendLine(CliEngine.s_commandExit);
this.disconnectCliMakeSure();
});
this._domButtonaSaveAnyway.on("click", () => {
this._domDialogCliErrors.close();
this.cliEngine.sendLine(CliEngine.s_commandSave);
this.disconnectCliMakeSure();
});
this._domButtonSaveBackup.on("click", () => this.onSaveConfigClick());
this._domButtonLoadBackup.on("click", () => this.onLoadConfigClick());
this._domButtonPresetSources.on("click", () => this.onPresetSourcesShowClick());
this._domButtonHideBackupWarning.on("click", () => this.onButtonHideBackupWarningClick());
this._domButtonSave.toggleClass(GUI.buttonDisabledClass, false);
this._domButtonCancel.toggleClass(GUI.buttonDisabledClass, false);
this._domReloadButton.on("click", () => this.reload());
this.enableSaveCancelButtons(false);
};
TABS.presets.enableSaveCancelButtons = function (isEnabled) {
this._domButtonSave.toggleClass(GUI.buttonDisabledClass, !isEnabled);
this._domButtonCancel.toggleClass(GUI.buttonDisabledClass, !isEnabled);
};
TABS.presets.onButtonHideBackupWarningClick = function() {
this._domWarningBackup.toggle(false);
ConfigStorage.set({ 'showPresetsWarningBackup': false });
};
TABS.presets.setupBackupWarning = function() {
const obj = ConfigStorage.get('showPresetsWarningBackup');
if (obj.showPresetsWarningBackup === undefined) {
obj.showPresetsWarningBackup = true;
}
const warningVisible = !!obj.showPresetsWarningBackup;
this._domWarningBackup.toggle(warningVisible);
};
TABS.presets.onPresetSourcesShowClick = function() {
this.presetsSourcesDialog.show().then(() => {
this.reload();
});
};
TABS.presets.onSaveConfigClick = function() {
const waitingDialog = GUI.showWaitDialog({title: i18n.getMessage("presetsLoadingDumpAll"), buttonCancelCallback: null});
const saveFailedDialogSettings = {
title: i18n.getMessage("warningTitle"),
text: i18n.getMessage("dumpAllNotSavedWarning"),
buttonConfirmText: i18n.getMessage("close"),
};
this.activateCli()
.then(() => this.readDumpAll())
.then(cliStrings => {
const prefix = 'cli_backup';
const suffix = 'txt';
const text = cliStrings.join("\n");
const filename = generateFilename(prefix, suffix);
return GUI.saveToTextFileDialog(text, filename, suffix);
})
.then(() => {
waitingDialog.close();
this.cliEngine.sendLine(CliEngine.s_commandExit);
})
.catch(() => {
waitingDialog.close();
return GUI.showInformationDialog(saveFailedDialogSettings);
})
.then(() => this.cliEngine.sendLine(CliEngine.s_commandExit));
};
TABS.presets.readDumpAll = function() {
let lastCliStringReceived = performance.now();
const diffAll = [CliEngine.s_commandDefaultsNoSave, ""];
const readingDumpIntervalName = "PRESETS_READING_DUMP_INTERVAL";
this.cliEngine.subscribeOnRowCame(str => {
lastCliStringReceived = performance.now();
if (CliEngine.s_commandDiffAll !== str && CliEngine.s_commandSave !== str) {
diffAll.push(str);
}
});
this.cliEngine.sendLine(CliEngine.s_commandDiffAll);
return new Promise(resolve => {
GUI.interval_add(readingDumpIntervalName, () => {
const currentTime = performance.now();
if (currentTime - lastCliStringReceived > 500) {
this.cliEngine.unsubscribeOnRowCame();
GUI.interval_remove(readingDumpIntervalName);
resolve(diffAll);
}
}, 500, false);
});
};
TABS.presets.onLoadConfigClick = function() {
GUI.readTextFileDialog("txt")
.then(text => {
if (text) {
const cliStrings = text.split("\n");
const pickedPreset = new PickedPreset({title: "user configuration"}, cliStrings);
this.pickedPresetList.push(pickedPreset);
this.onSaveClick();
}
});
};
TABS.presets.onHtmlLoad = function(callback) {
i18n.localizePage();
TABS.presets.adaptPhones();
this.readDom();
this.setupMenuButtons();
this.setupBackupWarning();
this._inputTextFilter.attr("placeholder", "example: \"karate racing\", or \"5'' freestyle\"");
this.presetsDetailedDialog = new PresetsDetailedDialog($("#presets_detailed_dialog"), this.pickedPresetList, () => this.onPresetPickedCallback());
this.presetsSourcesDialog = new PresetsSourcesDialog($("#presets_sources_dialog"));
this.presetsDetailedDialog.load()
.then(() => this.presetsSourcesDialog.load())
.then(() => {
this.tryLoadPresets();
GUI.content_ready(callback);
});
};
TABS.presets.onPresetPickedCallback = function() {
this.enableSaveCancelButtons(true);
};
TABS.presets.activateCli = function() {
return new Promise(resolve => {
CONFIGURATOR.cliEngineActive = true;
this.cliEngine.setUi($('#presets_cli_window'), $('#presets_cli_window_wrapper'), $('#presets_cli_command'));
this.cliEngine.enterCliMode();
GUI.timeout_add('presets_enter_cli_mode_done', () => {
resolve();
}, 500);
});
};
TABS.presets.reload = function() {
this.resetInitialValues();
this.tryLoadPresets();
};
TABS.presets.tryLoadPresets = function() {
const presetSource = this.presetsSourcesDialog.getActivePresetSource();
if (PresetSource.isUrlGithubRepo(presetSource.url)) {
this.presetsRepo = new PresetsGithubRepo(presetSource.url, presetSource.gitHubBranch);
} else {
this.presetsRepo = new PresetsWebsiteRepo(presetSource.url);
}
this._divMainContent.toggle(false);
this._divGlobalLoadingError.toggle(false);
this._divGlobalLoading.toggle(true);
this._domWarningNotOfficialSource.toggle(!this.presetsSourcesDialog.isOfficialActive);
this.presetsRepo.loadIndex().then(() => {
this.prepareFilterFields();
this._divGlobalLoading.toggle(false);
this._divMainContent.toggle(true);
}).catch(err => {
this._divGlobalLoading.toggle(false);
this._divGlobalLoadingError.toggle(true);
console.error(err);
});
};
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.preselectFilterFields();
this._inputTextFilter.on('input', () => this.updateSearchResults());
this._freezeSearch = false;
this.updateSearchResults();
};
TABS.presets.preselectFilterFields = function() {
this._selectCategory.multipleSelect('setSelects', ["TUNE"]);
const currentVersion = FC.CONFIG.flightControllerVersion;
const selectedVersions = [];
for(const bfVersion of this.presetsRepo.index.uniqueValues.firmware_version) {
if (currentVersion.startsWith(bfVersion)) {
selectedVersions.push(bfVersion);
}
}
this._selectFirmwareVersion.multipleSelect('setSelects', selectedVersions);
};
TABS.presets.prepareFilterSelectField = function(domSelectElement, selectOptions) {
domSelectElement.multipleSelect("destroy");
domSelectElement.multipleSelect({
data: selectOptions,
placeholder: i18n.getMessage("dropDownAll"),
onClick: () => { this.updateSearchResults(); },
onCheckAll: () => { this.updateSearchResults(); },
onUncheckAll: () => { this.updateSearchResults(); },
formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); },
formatAllSelected() { return i18n.getMessage("dropDownAll"); },
});
};
TABS.presets.updateSearchResults = function() {
if (!this._freezeSearch)
{
const searchParams = {
categories: this._selectCategory.multipleSelect("getSelects", "text"),
keywords: this._selectKeyword.multipleSelect("getSelects", "text"),
authors: this._selectAuthor.multipleSelect("getSelects", "text"),
firmwareVersions: this._selectFirmwareVersion.multipleSelect("getSelects", "text"),
status: this._selectStatus.multipleSelect("getSelects", "text"),
searchString: this._inputTextFilter.val().trim(),
};
searchParams.authors = searchParams.authors.map(str => str.toLowerCase());
const fitPresets = this.getFitPresets(searchParams);
this.displayPresets(fitPresets);
}
};
TABS.presets.displayPresets = function(fitPresets) {
this._presetPanels.forEach(presetPanel => {
presetPanel.remove();
});
this._presetPanels = [];
const maxPresetsToShow = 60;
this._domListTooManyFound.toggle(fitPresets.length > maxPresetsToShow);
fitPresets.length = Math.min(fitPresets.length, maxPresetsToShow);
this._domListNoFound.toggle(fitPresets.length === 0);
fitPresets.forEach(preset => {
const presetPanel = new PresetTitlePanel(this._divPresetList, preset, true);
presetPanel.load();
this._presetPanels.push(presetPanel);
presetPanel.subscribeClick(this.presetsDetailedDialog, this.presetsRepo);
});
this._domListTooManyFound.appendTo(this._divPresetList);
};
TABS.presets.getFitPresets = function(searchParams) {
const result = [];
for(const preset of this.presetsRepo.index.presets) {
if(this.isPresetFitSearch(preset, searchParams)) {
result.push(preset);
}
}
return result;
};
TABS.presets.isPresetFitSearchStatuses = function(preset, searchParams) {
return 0 === searchParams.status.length || searchParams.status.includes(preset.status);
};
TABS.presets.isPresetFitSearchCategories = function(preset, searchParams) {
if (0 !== searchParams.categories.length) {
if (undefined === preset.category) {
return false;
}
if (!searchParams.categories.includes(preset.category)) {
return false;
}
}
return true;
};
TABS.presets.isPresetFitSearchKeywords = function(preset, searchParams) {
if (0 !== searchParams.keywords.length) {
if (!Array.isArray(preset.keywords)) {
return false;
}
const keywordsIntersection = searchParams.keywords.filter(value => preset.keywords.includes(value));
if (0 === keywordsIntersection.length) {
return false;
}
}
return true;
};
TABS.presets.isPresetFitSearchAuthors = function(preset, searchParams) {
if (0 !== searchParams.authors.length) {
if (undefined === preset.author) {
return false;
}
if (!searchParams.authors.includes(preset.author.toLowerCase())) {
return false;
}
}
return true;
};
TABS.presets.isPresetFitSearchFirmwareVersions = function(preset, searchParams) {
if (0 !== searchParams.firmwareVersions.length) {
if (!Array.isArray(preset.firmware_version)) {
return false;
}
const firmwareVersionsIntersection = searchParams.firmwareVersions.filter(value => preset.firmware_version.includes(value));
if (0 === firmwareVersionsIntersection.length) {
return false;
}
}
return true;
};
TABS.presets.isPresetFitSearchString = function(preset, searchParams) {
if (searchParams.searchString)
{
const allKeywords = preset.keywords.join(" ");
const totalLine = [preset.description, allKeywords, preset.title, preset.author].join("\n").toLowerCase().replace("''", "\"");
const allWords = searchParams.searchString.toLowerCase().replace("''", "\"").split(" ");
for (const word of allWords) {
if (!totalLine.includes(word)) {
return false;
}
}
}
return true;
};
TABS.presets.isPresetFitSearch = function(preset, searchParams) {
if (preset.hidden) {
return false;
}
if (!this.isPresetFitSearchStatuses(preset, searchParams)) {
return false;
}
if (!this.isPresetFitSearchCategories(preset, searchParams)) {
return false;
}
if (!this.isPresetFitSearchKeywords(preset, searchParams)) {
return false;
}
if (!this.isPresetFitSearchAuthors(preset, searchParams)) {
return false;
}
if (!this.isPresetFitSearchFirmwareVersions(preset, searchParams)) {
return false;
}
if (!this.isPresetFitSearchString(preset, searchParams)) {
return false;
}
return true;
};
TABS.presets.adaptPhones = function() {
if (GUI.isCordova()) {
UI_PHONES.initToolbar();
}
};
TABS.presets.read = function(readInfo) {
TABS.presets.cliEngine.readSerial(readInfo);
};
TABS.presets.cleanup = function(callback) {
this.resetInitialValues();
if (!(CONFIGURATOR.connectionValid && CONFIGURATOR.cliEngineActive && CONFIGURATOR.cliEngineValid)) {
if (callback) {
callback();
}
return;
}
TABS.presets.cliEngine.close(() => {
if (callback) {
callback();
}
});
};
TABS.presets.resetInitialValues = function() {
CONFIGURATOR.cliEngineActive = false;
CONFIGURATOR.cliEngineValid = false;
TABS.presets.presetsRepo = null;
TABS.presets.pickedPresetList.length = 0;
this._domProgressDialog.close();
};
TABS.presets.expertModeChanged = function(expertModeEnabled) {
this._domShowHideCli.toggle(expertModeEnabled);
};

View file

@ -6311,6 +6311,11 @@ multipipe@^0.1.2:
dependencies: dependencies:
duplexer2 "0.0.2" duplexer2 "0.0.2"
multiple-select@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/multiple-select/-/multiple-select-1.5.2.tgz#a7290785fc10fa4b9f26d2a92937effc63648bc8"
integrity sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==
murmur-32@^0.1.0: murmur-32@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/murmur-32/-/murmur-32-0.1.0.tgz#c1a79d4fc5fabf0405749d0aff77c41402055861" resolved "https://registry.yarnpkg.com/murmur-32/-/murmur-32-0.1.0.tgz#c1a79d4fc5fabf0405749d0aff77c41402055861"
@ -10155,10 +10160,10 @@ yargs@~3.10.0:
decamelize "^1.0.0" decamelize "^1.0.0"
window-size "0.1.0" window-size "0.1.0"
yarn@^1.22.0: yarn@^1.22.17:
version "1.22.5" version "1.22.17"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.5.tgz#1933b7635429ca00847222dd9d38f05646e2df23" resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.17.tgz#bf910747d22497b573131f7341c0e1d15c74036c"
integrity sha512-5uzKXwdMc++mYktXqkfpNYT9tY8ViWegU58Hgbo+KXzrzzhEyP1Ip+BTtXloLrXNcNlxFJbLiFKGaS9vK9ym6Q== integrity sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ==
yauzl@2.4.1: yauzl@2.4.1:
version "2.4.1" version "2.4.1"