diff --git a/htdocs/css/files.css b/htdocs/css/files.css new file mode 100644 index 00000000..3d921a49 --- /dev/null +++ b/htdocs/css/files.css @@ -0,0 +1,33 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +html, body { + height: unset; +} + +body { + margin-bottom: 5rem; +} + +hr { + background: #444; +} + +h1 { + margin: 1em 0; + text-align: center; +} + +table { + border-collapse: separate; + border-spacing: 15px; +} + +td { + text-align: center; + border: 3px dotted; +} + +img { + width: 100%; +} diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 37b15391..45985b1c 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -158,8 +158,8 @@ input[type=range]:disabled { { height: 100%; position: relative; - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } #openwebrx-scale-container diff --git a/htdocs/files.html b/htdocs/files.html new file mode 100644 index 00000000..f277b8ce --- /dev/null +++ b/htdocs/files.html @@ -0,0 +1,17 @@ + + OpenWebRX+ Received Files + + + + + + + + ${header} +
+

OpenWebRX+ Received Files

+ + ${rows} +
+
+ diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index ea9de175..0a22ae72 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -11,6 +11,7 @@

Log

Receiver

Map
+
Files

Settings
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index a7a18c7d..8bbd70c4 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -297,10 +297,12 @@ SstvMessagePanel.prototype.pushMessage = function(msg) { // $b.scrollTop($b[0].scrollHeight); } else if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) { - var h = '' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + - ' ' + msg.sstvMode + '
'; - var c = ''; + var h = '
' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + + ' ' + msg.sstvMode + '
'; + var c = '
' + + '
'; // Append a new canvas $b.append($('' + h + c + '')); $b.scrollTop($b[0].scrollHeight); diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c2e86b8b..e74fbb66 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -90,6 +90,26 @@ function toggleRecording() { } } +function saveCanvas(canvas) { + // Get canvas by its ID + var c = document.getElementById(canvas); + if (c == null) return; + + // Create and click a link to the canvas data URL + var a = document.createElement('a'); + a.href = c.toDataURL('image/png'); + a.style = 'display: none'; + a.download = canvas + ".png"; + document.body.appendChild(a); + a.click(); + + // Get rid of the canvas data URL + setTimeout(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(a.href); + }, 0); +} + function zoomInOneStep() { zoom_set(zoom_level + 1); } diff --git a/owrx/config/core.py b/owrx/config/core.py index e22f004a..1134d86c 100644 --- a/owrx/config/core.py +++ b/owrx/config/core.py @@ -1,8 +1,12 @@ from owrx.config import ConfigError from configparser import ConfigParser import os +import re from glob import glob +import logging + +logger = logging.getLogger(__name__) class CoreConfig(object): defaults = { @@ -46,6 +50,28 @@ class CoreConfig(object): if not os.access(dir, os.W_OK): raise ConfigError(key, "{dir} is not writable".format(dir=dir)) + # Get complete path to a stored file from its filename by + # adding folder name + def getStoredFilePath(self, filename): + return self.get_temporary_directory() + "/" + filename + + # Get list of stored files, sorted in reverse alphabetic order + # (so that newer files appear first) + def getStoredFiles(self): + dir = self.get_temporary_directory() + files = [f for f in os.listdir(dir) if re.match(r"SSTV-[0-9]+-[0-9]+\.bmp", f)] + return sorted(files, reverse=True) + + # Delete all stored files except for newest ones + def cleanStoredFiles(self, keepN): + files = self.getStoredFiles() + for f in files[keepN:]: + logger.debug("Deleting stored file '%s'." % f) + try: + os.unlink(self.getStoredFilePath(f)) + except Exception as exptn: + logger.debug(str(exptn)) + def get_web_port(self): return self.web_port @@ -57,3 +83,4 @@ class CoreConfig(object): def get_aprs_symbols_path(self): return self.aprs_symbols_path + diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index e028e4db..e6685e86 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -162,6 +162,7 @@ defaultConfig = PropertyLayer( callsign_url="https://www.qrzcq.com/call/{}", usage_policy_url="policy", session_timeout=0, + keep_files=20, decoding_queue_workers=2, decoding_queue_length=10, wsjt_decoding_depth=3, diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py new file mode 100644 index 00000000..c29653d0 --- /dev/null +++ b/owrx/controllers/file.py @@ -0,0 +1,38 @@ +from owrx.controllers.template import WebpageController +from owrx.controllers.assets import AssetsController +from owrx.config.core import CoreConfig + +class FileController(AssetsController): + def getFilePath(self, file): + return CoreConfig().getStoredFilePath(file) + + +class FilesController(WebpageController): + def template_variables(self): + files = CoreConfig().getStoredFiles() + rows = "" + + for i in range(len(files)): + # Start a row + if i % 3 == 0: + rows += '\n' + # Print out individual tiles + rows += ('' + + ('' % (files[i], files[i])) + + ('' % (files[i], files[i])) + + ('

%s

' % files[i]) + + '
\n') + # Finish a row + if i % 3 == 2: + rows += '\n' + + # Finish final row + if len(files) > 0 and len(files) % 3 != 0: + rows += '\n' + + variables = super().template_variables() + variables["rows"] = rows + return variables + + def indexAction(self): + self.serve_template("files.html", **self.template_variables()) diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py index fa674166..a96eea1c 100644 --- a/owrx/controllers/settings/general.py +++ b/owrx/controllers/settings/general.py @@ -70,18 +70,24 @@ class GeneralSettingsController(SettingsFormController): NumberInput( "max_clients", "Maximum number of clients", + infotext="Number of people who can connect at the same time.", + ), + NumberInput( + "keep_files", + "Maximum number of files", + infotext="Number of received images and other files to keep.", ), NumberInput( "session_timeout", "Session timeout", - infotext="User session timeout in seconds (0 to disable timeout).", + infotext="Client session timeout in seconds (0 to disable timeout).", append="secs", ), TextInput( "usage_policy_url", "Usage policy URL", infotext="Specifies web page describing receiver usage policy " - + "and shown when the user session times out.", + + "and shown when a client session times out.", ), ), Section( diff --git a/owrx/details.py b/owrx/details.py index 5d979b97..ba672029 100644 --- a/owrx/details.py +++ b/owrx/details.py @@ -19,7 +19,8 @@ class ReceiverDetails(PropertyFilter): "photo_title", "photo_desc", "usage_policy_url", - "session_timeout", + "session_timeout", + "keep_files", ) ) diff --git a/owrx/http.py b/owrx/http.py index 347bacff..ebb35b70 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,6 +1,7 @@ from owrx.controllers.status import StatusController from owrx.controllers.template import IndexController, MapController, PolicyController from owrx.controllers.feature import FeatureController +from owrx.controllers.file import FilesController, FileController from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, CompiledAssetsController from owrx.controllers.websocket import WebSocketController from owrx.controllers.api import ApiController @@ -96,6 +97,8 @@ class Router(object): StaticRoute("/map", MapController), StaticRoute("/policy", PolicyController), StaticRoute("/features", FeatureController), + StaticRoute("/files", FilesController), + RegexRoute("^/files/(SSTV-[0-9]+-[0-9]+\.bmp)$", FileController), StaticRoute("/api/features", ApiController), StaticRoute("/metrics", MetricsController, options={"action": "prometheusAction"}), StaticRoute("/metrics.json", MetricsController), diff --git a/owrx/sstv.py b/owrx/sstv.py index b7db9ab5..5da7d422 100644 --- a/owrx/sstv.py +++ b/owrx/sstv.py @@ -1,4 +1,5 @@ from owrx.config.core import CoreConfig +from owrx.config import Config from csdr.module import ThreadModule from pycsdr.types import Format from datetime import datetime @@ -80,7 +81,14 @@ class SstvParser(ThreadModule): if self.height==0 or self.line