diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index 038c25c..24aa8fc 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -85,4 +85,21 @@ table.bookmarks .frequency { .sdr-device-list .list-group-item, .sdr-profile-list .list-group-item { background: initial; +} + +.removable-group.removable { + display: flex; + flex-direction: row; +} + +.removable-group.removable .removable-item { + flex: 1 0 auto; +} + +.removable-group.removable .option-remove-button { + flex: 0 0 70px; +} + +.option-add-button, .option-remove-button { + width: 70px; } \ No newline at end of file diff --git a/htdocs/lib/settings/OptionalSection.js b/htdocs/lib/settings/OptionalSection.js new file mode 100644 index 0000000..8c60fa4 --- /dev/null +++ b/htdocs/lib/settings/OptionalSection.js @@ -0,0 +1,26 @@ +$.fn.optionalSection = function(){ + this.each(function() { + var $section = $(this); + var $select = $section.find('.optional-select'); + var $optionalInputs = $section.find('.optional-inputs'); + $section.on('click', '.option-add-button', function(e){ + var field = $select.val(); + var group = $optionalInputs.find(".form-group[data-field='" + field + "']"); + group.find('input, select').prop('disabled', false); + $select.parents('.form-group').before(group); + $select.find('option[value=\'' + field + '\']').remove(); + + return false; + }); + $section.on('click', '.option-remove-button', function(e) { + var group = $(e.target).parents('.form-group') + group.find('input, select').prop('disabled', true); + $optionalInputs.append(group); + var $label = group.find('> label'); + var $option = $(''); + $select.append($option); + + return false; + }) + }); +} \ No newline at end of file diff --git a/htdocs/settings.js b/htdocs/settings.js index 4a9903b..30aacad 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -5,4 +5,5 @@ $(function(){ $('.wsjt-decoding-depths').wsjtDecodingDepthsInput(); $('#waterfall_scheme').waterfallDropdown(); $('#rf_gain').gainInput(); + $('.optional-section').optionalSection(); }); \ No newline at end of file diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index f090fb6..696d541 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -151,6 +151,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/settings/WsjtDecodingDepthsInput.js", "lib/settings/WaterfallDropdown.js", "lib/settings/GainInput.js", + "lib/settings/OptionalSection.js", "settings.js", ], } diff --git a/owrx/controllers/settings/__init__.py b/owrx/controllers/settings/__init__.py index 11f056c..12d8b09 100644 --- a/owrx/controllers/settings/__init__.py +++ b/owrx/controllers/settings/__init__.py @@ -10,19 +10,25 @@ class Section(object): self.title = title self.inputs = inputs + def render_input(self, input, data): + return input.render(data) + def render_inputs(self, data): - return "".join([i.render(data) for i in self.inputs]) + return "".join([self.render_input(i, data) for i in self.inputs]) + + def classes(self): + return ["col-12", "settings-section"] def render(self, data): return """ -
+

{title}

{inputs}
""".format( - title=self.title, inputs=self.render_inputs(data) + classes=" ".join(self.classes()), title=self.title, inputs=self.render_inputs(data) ) def parse(self, data): diff --git a/owrx/controllers/settings/sdr.py b/owrx/controllers/settings/sdr.py index 90bb131..926001e 100644 --- a/owrx/controllers/settings/sdr.py +++ b/owrx/controllers/settings/sdr.py @@ -71,7 +71,7 @@ class SdrDeviceController(SettingsFormController): def getSections(self): try: description = SdrDeviceDescription.getByType(self.device["type"]) - return [description.getSection(self.device)] + return [description.getSection()] except SdrDeviceDescriptionMissing: # TODO provide a generic interface that allows to switch the type return [] diff --git a/owrx/form/__init__.py b/owrx/form/__init__.py index fbbbc42..8a88b5d 100644 --- a/owrx/form/__init__.py +++ b/owrx/form/__init__.py @@ -6,11 +6,19 @@ from enum import Enum class Input(ABC): - def __init__(self, id, label, infotext=None, converter: Converter = None): + def __init__(self, id, label, infotext=None, converter: Converter = None, disabled=False, removable=False): self.id = id self.label = label self.infotext = infotext self.converter = self.defaultConverter() if converter is None else converter + self.disabled = disabled + self.removable = removable + + def setDisabled(self, disabled=True): + self.disabled = disabled + + def setRemovable(self, removable=True): + self.removable = removable def defaultConverter(self): return NullConverter() @@ -18,23 +26,47 @@ class Input(ABC): def bootstrap_decorate(self, input): infotext = "{text}".format(text=self.infotext) if self.infotext else "" return """ -
+
-
- {input} - {infotext} +
+
+ {input} + {infotext} +
+ {removebutton}
""".format( - id=self.id, label=self.label, input=input, infotext=infotext + id=self.id, + label=self.label, + input=input, + infotext=infotext, + removable="removable" if self.removable else "", + removebutton='' + if self.removable + else "", ) def input_classes(self): return " ".join(["form-control", "form-control-sm"]) - @abstractmethod + def input_properties(self, value): + props = { + "class": self.input_classes(), + "id": self.id, + "name": self.id, + "placeholder": self.label, + "value": value, + } + if self.disabled: + props["disabled"] = "disabled" + return props + + def render_input_properties(self, value): + return " ".join('{}="{}"'.format(prop, value) for prop, value in self.input_properties(value).items()) + def render_input(self, value): - pass + return "".format(properties=self.render_input_properties(value)) def render(self, config): value = config[self.id] if self.id in config else None @@ -43,14 +75,15 @@ class Input(ABC): def parse(self, data): return {self.id: self.converter.convert_from_form(data[self.id][0])} if self.id in data else {} + def getLabel(self): + return self.label + class TextInput(Input): - def render_input(self, value): - return """ - - """.format( - id=self.id, label=self.label, classes=self.input_classes(), value=value - ) + def input_properties(self, value): + props = super().input_properties(value) + props["type"] = "text" + return props class NumberInput(Input): @@ -62,6 +95,13 @@ class NumberInput(Input): def defaultConverter(self): return IntConverter() + def input_properties(self, value): + props = super().input_properties(value) + props["type"] = "number" + if self.step: + props["step"] = self.step + return props + def render_input(self, value): if self.append: append = """ @@ -76,15 +116,11 @@ class NumberInput(Input): return """
- + {input} {append}
""".format( - id=self.id, - label=self.label, - classes=self.input_classes(), - value=value, - step='step="{0}"'.format(self.step) if self.step else "", + input=super().render_input(value), append=append, ) @@ -116,13 +152,15 @@ class LocationInput(Input): def render_sub_input(self, value, id): return """
- +
""".format( id="{0}-{1}".format(self.id, id), label=self.label, classes=self.input_classes(), value=value[id], + disabled="disabled" if self.disabled else "", ) def parse(self, data): @@ -132,9 +170,9 @@ class LocationInput(Input): class TextAreaInput(Input): def render_input(self, value): return """ - + """.format( - id=self.id, classes=self.input_classes(), value=value + id=self.id, classes=self.input_classes(), value=value, disabled="disabled" if self.disabled else "" ) @@ -145,16 +183,17 @@ class CheckboxInput(Input): def render_input(self, value): return """ -
- - -
+
+ + +
""".format( id=self.id, classes=self.input_classes(), checked="checked" if value else "", + disabled="disabled" if self.disabled else "", checkboxText=self.checkboxText, ) @@ -164,6 +203,9 @@ class CheckboxInput(Input): def parse(self, data): return {self.id: self.converter.convert_from_form(self.id in data and data[self.id][0] == "on")} + def getLabel(self): + return self.checkboxText + class Option(object): # used for both MultiCheckboxInput and DropdownInput @@ -186,7 +228,7 @@ class MultiCheckboxInput(Input): def render_checkbox(self, option, value): return """
- + @@ -196,6 +238,7 @@ class MultiCheckboxInput(Input): classes=self.input_classes(), checked="checked" if option.value in value else "", checkboxText=option.text, + disabled="disabled" if self.disabled else "", ) def parse(self, data): @@ -242,9 +285,12 @@ class DropdownInput(Input): def render_input(self, value): return """ - + """.format( - classes=self.input_classes(), id=self.id, options=self.render_options(value) + classes=self.input_classes(), + id=self.id, + options=self.render_options(value), + disabled="disabled" if self.disabled else "", ) def render_options(self, value): diff --git a/owrx/form/device.py b/owrx/form/device.py index cd987f1..d1c0c92 100644 --- a/owrx/form/device.py +++ b/owrx/form/device.py @@ -16,12 +16,12 @@ class GainInput(Input): return """
- {options} {stageoption}
@@ -32,6 +32,7 @@ class GainInput(Input): label=self.label, options=self.render_options(value), stageoption="" if self.gain_stages is None else self.render_stage_option(value), + disabled="disabled" if self.disabled else "" ) def render_options(self, value): @@ -79,15 +80,16 @@ class GainInput(Input): inputs="".join( """
-
{stage}
+ + class="col-9 {classes}" placeholder="{stage}" step="any" {disabled}>
""".format( id=self.id, stage=stage, value=value_dict[stage] if stage in value_dict else "", classes=self.input_classes(), + disabled="disabled" if self.disabled else "", ) for stage in self.gain_stages ) diff --git a/owrx/form/wsjt.py b/owrx/form/wsjt.py index a5e1af0..215b1b7 100644 --- a/owrx/form/wsjt.py +++ b/owrx/form/wsjt.py @@ -22,7 +22,7 @@ class Q65ModeMatrix(Input): id=self.checkbox_id(mode, interval), checked="checked" if "{}{}".format(mode.name, interval.value) in value else "", checkboxText="Mode {} interval {}s".format(mode.name, interval.value), - disabled="" if interval.is_available(mode) else "disabled", + disabled="" if interval.is_available(mode) and not self.disabled else "disabled", ) def render_input(self, value): @@ -69,7 +69,7 @@ class WsjtDecodingDepthsInput(Input): ) return """ - +