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
+
+
+
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