From ce9d93d4f34859f21f9be6ac3c9adf3da6aac79c Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Mon, 28 Oct 2024 14:32:01 -0400 Subject: [PATCH] Added underlying modulation to bookmarks, refactored bookmarks. --- htdocs/css/admin.css | 22 +++++- htdocs/css/openwebrx.css | 15 ++-- htdocs/index.html | 6 +- htdocs/lib/BookmarkBar.js | 55 +++++++++++--- htdocs/lib/BookmarkDialog.js | 17 ++++- htdocs/lib/Modes.js | 4 +- htdocs/lib/settings/BookmarkTable.js | 100 ++++++++++++++++--------- owrx/bookmarks.py | 5 ++ owrx/controllers/settings/bookmarks.py | 98 +++++++++++++++++++----- 9 files changed, 239 insertions(+), 83 deletions(-) diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index c9df225c..1079c548 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -82,7 +82,8 @@ h1 { /* col-1 */ .bookmarks table [data-editor="name"] { - width: 35%; + white-space: nowrap; + width: 29%; } /* col-2 */ @@ -101,15 +102,28 @@ h1 { } /* col-4 */ -.bookmarks table [data-editor="description"] { - width: 35%; +.bookmarks table [data-editor="underlying"] { + white-space: nowrap; + min-width: 160px; + width: 10%; } /* col-5 */ +.bookmarks table [data-editor="description"] { + width: 29%; +} + +/* col-6 */ +.bookmarks table [data-editor="scannable"] { + width: 2%; + text-align: center; +} + +/* col-7 */ .bookmarks table tr td:last-child, .bookmarks table tr th:last-child { text-align: right; width: 10%; - } +} .bookmarks table input, .bookmarks table select { text-align: inherit; diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 76e46fff..a38f0bb6 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -1512,17 +1512,16 @@ img.openwebrx-mirror-img } .openwebrx-dialog { - background-color: #575757; - padding: 10px; - color: white; - position: fixed; - font-size: 10pt; - border-radius: 15px; - -moz-border-radius: 15px; + background-color: #575757; + padding: 10px; + color: white; + font-size: 10pt; + border-radius: 15px; + -moz-border-radius: 15px; position: fixed; left: 50%; top: 50%; - transform: translate(-50%, 0); + transform: translate(-50%, -50%); } .openwebrx-dialog .form-field { diff --git a/htdocs/index.html b/htdocs/index.html index b75b02ba..16873c1b 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -378,12 +378,16 @@
- +
+
+ + +
diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js index 65ca2040..77048e04 100644 --- a/htdocs/lib/BookmarkBar.js +++ b/htdocs/lib/BookmarkBar.js @@ -2,7 +2,7 @@ function BookmarkBar() { var me = this; me.modesToScan = ['lsb', 'usb', 'cw', 'am', 'sam', 'nfm']; me.localBookmarks = new BookmarkLocalStorage(); - me.$container = $("#openwebrx-bookmarks-container"); + me.$container = $('#openwebrx-bookmarks-container'); me.bookmarks = {}; me.$container.on('click', '.bookmark', function(e){ @@ -11,9 +11,7 @@ function BookmarkBar() { var b = $bookmark.data(); if (!b || !b.frequency || !b.modulation) return; me.getDemodulator().set_offset_frequency(b.frequency - center_freq); - if (b.modulation) { - me.getDemodulatorPanel().setMode(b.modulation, b.underlying); - } + me.getDemodulatorPanel().setMode(b.modulation, b.underlying); $bookmark.addClass('selected'); stopScanner(); }); @@ -41,7 +39,7 @@ function BookmarkBar() { me.showEditDialog(); }); - me.$dialog = $("#openwebrx-dialog-bookmark"); + me.$dialog = $('#openwebrx-dialog-bookmark'); me.$dialog.find('.openwebrx-button[data-action=cancel]').click(function(){ me.$dialog.hide(); }); @@ -108,25 +106,60 @@ BookmarkBar.prototype.render = function(){ BookmarkBar.prototype.showEditDialog = function(bookmark) { if (!bookmark) { - var mode = this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation(); + var mode1 = this.getDemodulator().get_secondary_demod() + var mode2 = this.getDemodulator().get_modulation(); + // if no secondary demod, use the primary one + if (!mode1) { mode1 = mode2; mode2 = ''; } bookmark = { - name: "", + name: '', frequency: center_freq + this.getDemodulator().get_offset_frequency(), - modulation: mode, - description: "", - scannable : this.modesToScan.indexOf(mode) >= 0 + modulation: mode1, + underlying: mode2, + description: '', + scannable : this.modesToScan.indexOf(mode1) >= 0 } + this.sanitizeBookmark(bookmark); } this.$dialog.bookmarkDialog().setValues(bookmark); this.$dialog.show(); this.$dialog.find('#name').focus(); }; +BookmarkBar.prototype.sanitizeBookmark = function(b) { + // must have name, frequency, and modulation + if (!b.name || !b.frequency || !b.modulation) + return "Must have name, frequency, and modulation."; + + // must have non-empty name + b.name = b.name.trim(); + if (b.name.length <= 0) return "Must have a non-empty name."; + + // must have positive frequency + b.frequency = Number(b.frequency); + if (b.frequency <= 0) return "Frequency must be positive."; + + // must have valid modulation + var mode = Modes.findByModulation(b.modulation); + if (!mode) return "Must have valid modulation." + + // check that underlying demodulator is valid + if (!b.underlying) + b.underlying = ''; + else if (!mode.underlying) + return "Must not have underlying modulation."; + else if (mode.underlying.indexOf(b.underlying) < 0) + return "Must have valid underlying modulation."; + + return null; +}; + BookmarkBar.prototype.storeBookmark = function() { var me = this; var bookmark = this.$dialog.bookmarkDialog().getValues(); if (!bookmark) return; - bookmark.frequency = Number(bookmark.frequency); + + var error = this.sanitizeBookmark(bookmark); + if (error) { alert(error); return; } var bookmarks = me.localBookmarks.getBookmarks(); diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js index ee7c2fd4..d6b2a71d 100644 --- a/htdocs/lib/BookmarkDialog.js +++ b/htdocs/lib/BookmarkDialog.js @@ -2,16 +2,25 @@ $.fn.bookmarkDialog = function() { var $el = this; return { setModes: function(modes) { - $el.find('#modulation').html(modes.filter(function(m){ + $el.find('#modulation').html(modes.filter(function(m) { return m.isAvailable(); }).map(function(m) { return ''; }).join('')); return this; }, + setUnderlying: function(modes) { + $el.find('#underlying').html('' + + modes.filter(function(m) { + return m.isAvailable() && !m.underlying && m.type === 'analog'; + }).map(function(m) { + return ''; + }).join('')); + return this; + }, setValues: function(bookmark) { var $form = $el.find('form'); - ['name', 'frequency', 'modulation', 'description', 'scannable'].forEach(function(key){ + ['name', 'frequency', 'modulation', 'underlying', 'description', 'scannable'].forEach(function(key) { var $input = $form.find('#' + key); if ($input.is(':checkbox')) { $input.prop('checked', bookmark[key]); @@ -25,7 +34,7 @@ $.fn.bookmarkDialog = function() { getValues: function() { var bookmark = {}; var valid = true; - ['name', 'frequency', 'modulation', 'description', 'scannable'].forEach(function(key){ + ['name', 'frequency', 'modulation', 'underlying', 'description', 'scannable'].forEach(function(key) { var $input = $el.find('#' + key); valid = valid && $input[0].checkValidity(); bookmark[key] = $input.is(':checkbox')? $input.is(':checked') : $input.val(); @@ -38,4 +47,4 @@ $.fn.bookmarkDialog = function() { return bookmark; } } -} +}; diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js index 0bdac90b..1ec050d1 100644 --- a/htdocs/lib/Modes.js +++ b/htdocs/lib/Modes.js @@ -5,7 +5,9 @@ var Modes = { setModes:function(json){ this.modes = json.map(function(m){ return new Mode(m); }); this.updatePanels(); - $('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes); + var bookmarkDialog = $('#openwebrx-dialog-bookmark').bookmarkDialog(); + bookmarkDialog.setUnderlying(this.modes); + bookmarkDialog.setModes(this.modes); }, getModes:function(){ return this.modes; diff --git a/htdocs/lib/settings/BookmarkTable.js b/htdocs/lib/settings/BookmarkTable.js index f251c9a6..f9e73cf6 100644 --- a/htdocs/lib/settings/BookmarkTable.js +++ b/htdocs/lib/settings/BookmarkTable.js @@ -177,8 +177,8 @@ ModulationEditor.prototype = new Editor(); ModulationEditor.prototype.getInputHtml = function() { return ''; }; @@ -188,6 +188,30 @@ ModulationEditor.prototype.getHtml = function() { return $option.html(); }; +function UnderlyingEditor(table) { + Editor.call(this, table); + this.modes = table.data('modes'); +} + +UnderlyingEditor.prototype = new Editor(); + +UnderlyingEditor.prototype.getInputHtml = function() { + return ''; +}; + +UnderlyingEditor.prototype.getHtml = function() { + var $option = this.input.find('option:selected') + return $option? $option.html() : ''; +}; + function DescriptionEditor(table) { Editor.call(this, table); } @@ -220,16 +244,21 @@ ScannableEditor.prototype.getHtml = function() { return this.getValue()? '✓' : ''; }; +var renderModulation = function(m, modes) { + return !m? 'None' : m in modes? modes[m].name : m; +} + $.fn.bookmarktable = function() { var editors = { name: NameEditor, frequency: FrequencyEditor, modulation: ModulationEditor, + underlying: UnderlyingEditor, description: DescriptionEditor, scannable: ScannableEditor }; - $.each(this, function(){ + $.each(this, function() { var $table = $(this).find('table'); $table.on('dblclick', 'td', function(e) { @@ -252,9 +281,11 @@ $.fn.bookmarktable = function() { data: JSON.stringify(Object.fromEntries([[name, editor.getValue()]])), contentType: 'application/json', method: 'POST' - }).done(function(){ + }).done(function() { $cell.data('value', editor.getValue()); $cell.html(editor.getHtml()); + }).fail(function() { + $cell.html(html); }); }; @@ -337,7 +368,10 @@ $.fn.bookmarktable = function() { data: JSON.stringify([data]), contentType: 'application/json', method: 'POST' - }).done(function(data){ + }).fail(function(data) { + // adding failed, reenable inputs + $.map(inputs, function(input, name) { input.disable(false); }); + }).done(function(data) { if (data.length && data.length === 1 && 'bookmark_id' in data[0]) { row.attr('data-id', data[0]['bookmark_id']); var tds = row.find('td'); @@ -347,16 +381,16 @@ $.fn.bookmarktable = function() { td.data('value', input.getValue()); td.html(input.getHtml()); }); + } - var $cell = row.find('td').last(); - var $group = $cell.find('.btn-group'); - if ($group.length) { - $group.remove; - $cell.html('
delete
'); - } + // remove inputs + var $cell = row.find('td').last(); + var $group = $cell.find('.btn-group'); + if ($group.length) { + $group.remove; + $cell.html('
delete
'); } }); - }); $table.append(row); @@ -372,16 +406,13 @@ $.fn.bookmarktable = function() { var modes = $table.data('modes'); var $list = $(''); $list.append(bookmarks.map(function(b) { - var modulation = b.modulation; - if (modulation in modes) { - modulation = modes[modulation]; - } var row = $( '' + - '' + + '' + '' + '' + - '' + + '' + +// '' + '' ); row.data('bookmark', b); @@ -407,31 +438,30 @@ $.fn.bookmarktable = function() { data: JSON.stringify(selected), contentType: 'application/json', method: 'POST' - }).done(function(data){ + }).fail(function(data) { + // import failed + $table.find('.emptytext').remove(); + }).done(function(data) { $table.find('.emptytext').remove(); var modes = $table.data('modes'); if (data.length && data.length == selected.length) { $table.append(data.map(function(obj, index) { - var bookmark = selected[index]; - var modulation_name = bookmark.modulation; - if (modulation_name in modes) { - modulation_name = modes[modulation_name]; - } - // provide reasonable default for missing fields - if (!('description' in bookmark)) { - bookmark.description = ''; - } - if (!('scannable' in bookmark)) { + var b = selected[index]; + // provide reasonable defaults for missing fields + if (!('underlying' in b)) b.underlying = ''; + if (!('description' in b)) b.description = ''; + if (!('scannable' in b)) { var modesToScan = ['lsb', 'usb', 'cw', 'am', 'sam', 'nfm']; - bookmark.scannable = modesToScan.indexOf(bookmark.modulation) >= 0; + b.scannable = modesToScan.indexOf(b.modulation) >= 0; } return $( '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + '' + diff --git a/owrx/bookmarks.py b/owrx/bookmarks.py index b7b3aeea..81f7b93a 100644 --- a/owrx/bookmarks.py +++ b/owrx/bookmarks.py @@ -16,6 +16,7 @@ class Bookmark(object): self.name = j["name"] self.frequency = j["frequency"] self.modulation = j["modulation"] + self.underlying = j["underlying"] if "underlying" in j else "" self.description = j["description"] if "description" in j else "" self.srcFile = srcFile # By default, only scan modulations that make sense to scan @@ -33,6 +34,9 @@ class Bookmark(object): def getModulation(self): return self.modulation + def getUnderlying(self): + return self.underlying + def getDescription(self): return self.description @@ -47,6 +51,7 @@ class Bookmark(object): "name": self.getName(), "frequency": self.getFrequency(), "modulation": self.getModulation(), + "underlying": self.getUnderlying(), "description": self.getDescription(), "scannable": self.isScannable(), } diff --git a/owrx/controllers/settings/bookmarks.py b/owrx/controllers/settings/bookmarks.py index 1c9c9a09..f2e6499c 100644 --- a/owrx/controllers/settings/bookmarks.py +++ b/owrx/controllers/settings/bookmarks.py @@ -2,7 +2,7 @@ from owrx.controllers.template import WebpageController from owrx.controllers.admin import AuthorizationMixin from owrx.controllers.settings import SettingsBreadcrumb from owrx.bookmarks import Bookmark, Bookmarks -from owrx.modes import Modes +from owrx.modes import Modes, AnalogMode from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin import json import math @@ -24,7 +24,7 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController def render_table(self): bookmarks = Bookmarks.getSharedInstance().getEditableBookmarks() emptyText = """ - """ @@ -35,6 +35,7 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController + @@ -43,7 +44,11 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
 ' + b.name + '' + renderFrequency(b.frequency) + '' + modulation + '' + renderModulation(b.modulation, modes) + '' + renderModulation(b.underlying, modes) + '
' + bookmark.name + '' + renderFrequency(bookmark.frequency) +'' + modulation_name + '' + bookmark.description + '' + (bookmark.scannable? '✓':'') + '' + b.name + '' + renderFrequency(b.frequency) +'' + renderModulation(b.modulation, modes) + '' + renderModulation(b.underlying, modes) + '' + b.description + '' + (b.scannable? '✓':'') + '' + '' + '
+
No bookmarks in storage. You can add new bookmarks using the buttons below.
Name Frequency ModulationUnderlying Description Scan Actions
""".format( bookmarks="".join(self.render_bookmark(b) for b in bookmarks) if bookmarks else emptyText, - modes=json.dumps({m.modulation: m.name for m in Modes.getAvailableModes()}), + modes=json.dumps({m.modulation: { + "name" : m.name, + "analog" : isinstance(m, AnalogMode), + "underlying" : m.underlying if hasattr(m, "underlying") else [] + } for m in Modes.getAvailableModes()}), ) def render_bookmark(self, bookmark: Bookmark): @@ -65,13 +70,18 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController suffix = suffixes[exp] return "{num:g} {suffix}Hz".format(num=num, suffix=suffix) - mode = Modes.findByModulation(bookmark.getModulation()) - scan = bookmark.isScannable() + scan = bookmark.isScannable() + name1 = bookmark.getModulation() + name2 = bookmark.getUnderlying() + mode1 = Modes.findByModulation(name1) + mode2 = Modes.findByModulation(name2) + return """ {name} {rendered_frequency} {modulation_name} + {underlying_name} {description} {scannable_check} @@ -84,8 +94,10 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController # TODO render frequency in si units frequency=bookmark.getFrequency(), rendered_frequency=render_frequency(bookmark.getFrequency()), - modulation=bookmark.getModulation() if mode is None else mode.modulation, - modulation_name=bookmark.getModulation() if mode is None else mode.name, + modulation=name1 if mode1 is None else mode1.modulation, + underlying=name2 if mode2 is None else mode2.modulation, + modulation_name=name1 if mode1 is None else mode1.name, + underlying_name="None" if not name2 else name2 if mode2 is None else mode2.name, description=bookmark.getDescription(), scannable="true" if scan else "false", scannable_check="✓" if scan else "", @@ -98,6 +110,45 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController except StopIteration: return None + def _sanitizeBookmark(self, data): + try: + # Must have name, frequency, modulation + if "name" not in data or "frequency" not in data or "modulation" not in data: + return "Bookmark missing required fields" + # Name must not be empty + data["name"] = data["name"].strip() + if len(data["name"]) == 0: + return "Empty bookmark name" + # Frequency must be integer + if not isinstance(data["frequency"], int): + data["frequency"] = int(data["frequency"]) + # Frequency must be >0 + if data["frequency"] <= 0: + return "Frequency must be positive" + # Get both modes + mode1 = Modes.findByModulation(data["modulation"]) if "modulation" in data else None + mode2 = Modes.findByModulation(data["underlying"]) if "underlying" in data else None + # Unknown main mode + if mode1 is None: + return "Invalid modulation" + # No underlying mode + if mode2 is None: + data["underlying"] = "" + else: + # Main mode has no underlying mode or underlying mode incorrect + if not hasattr(mode1, "underlying") or mode2.modulation not in mode1.underlying: + return "Incorrect underlying modulation" + # Underlying mode is at the default value + #if mode2.modulation == mode1.underlying[0]: + # data["underlying"] = "" + + except Exception as e: + # Something else went horribly wrong + return str(e) + + # Everything ok + return None + def update(self): bookmark_id = int(self.request.matches.group(1)) bookmark = self._findBookmark(bookmark_id) @@ -105,18 +156,26 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController self.send_response("{}", content_type="application/json", code=404) return try: + newd = {} data = json.loads(self.get_body().decode("utf-8")) - for key in ["name", "frequency", "modulation", "description", "scannable"]: + for key in ["name", "frequency", "modulation", "underlying", "description", "scannable"]: if key in data: - value = data[key] - if key == "frequency": - value = int(value) - setattr(bookmark, key, value) + newd[key] = data[key] + elif hasattr(bookmark, key): + newd[key] = getattr(bookmark, key) + # Make sure everything is correct + error = self._sanitizeBookmark(newd) + if error is not None: + raise ValueError(error) + # Update and store bookmark + for key in newd: + setattr(bookmark, key, newd[key]) Bookmarks.getSharedInstance().store() # TODO this should not be called explicitly... bookmarks don't have any event capability right now, though Bookmarks.getSharedInstance().notifySubscriptions(bookmark) self.send_response("{}", content_type="application/json", code=200) - except json.JSONDecodeError: + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed updating bookmark: " + str(e)) self.send_response("{}", content_type="application/json", code=400) def new(self): @@ -125,12 +184,12 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController def create(bookmark_data): # sanitize data = {} - for key in ["name", "frequency", "modulation", "description", "scannable"]: + for key in ["name", "frequency", "modulation", "underlying", "description", "scannable"]: if key in bookmark_data: - if key == "frequency": - data[key] = int(bookmark_data[key]) - else: - data[key] = bookmark_data[key] + data[key] = bookmark_data[key] + error = self._sanitizeBookmark(data) + if error is not None: + raise ValueError(error) bookmark = Bookmark(data) bookmarks.addBookmark(bookmark) return {"bookmark_id": id(bookmark)} @@ -140,7 +199,8 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController result = [create(b) for b in data] bookmarks.store() self.send_response(json.dumps(result), content_type="application/json", code=200) - except json.JSONDecodeError: + except (json.JSONDecodeError, ValueError) as e: + logger.warning("Failed creating bookmark: " + str(e)) self.send_response("{}", content_type="application/json", code=400) def delete(self):