refactor: convert WEFAX plugin to TypeScript
This commit is contained in:
@@ -10,7 +10,7 @@ const pluginGroups = {
|
|||||||
};
|
};
|
||||||
const loaded = /* @__PURE__ */ new Set();
|
const loaded = /* @__PURE__ */ new Set();
|
||||||
const loading = /* @__PURE__ */ new Map();
|
const loading = /* @__PURE__ */ new Map();
|
||||||
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]);
|
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]);
|
||||||
function loadLegacyScript(path) {
|
function loadLegacyScript(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
var wefaxDom = {
|
(() => {
|
||||||
|
// src/plugins/wefax.ts
|
||||||
|
var wefaxWindow = window;
|
||||||
|
var wefaxDom = {
|
||||||
status: document.getElementById("wefax-status"),
|
status: document.getElementById("wefax-status"),
|
||||||
liveView: document.getElementById("wefax-live-view"),
|
liveView: document.getElementById("wefax-live-view"),
|
||||||
historyView: document.getElementById("wefax-history-view"),
|
historyView: document.getElementById("wefax-history-view"),
|
||||||
@@ -15,34 +18,34 @@ var wefaxDom = {
|
|||||||
clearBtn: document.getElementById("wefax-clear-btn"),
|
clearBtn: document.getElementById("wefax-clear-btn"),
|
||||||
viewLiveBtn: document.getElementById("wefax-view-live"),
|
viewLiveBtn: document.getElementById("wefax-view-live"),
|
||||||
viewHistoryBtn: document.getElementById("wefax-view-history")
|
viewHistoryBtn: document.getElementById("wefax-view-history")
|
||||||
};
|
};
|
||||||
var wefaxImageHistory = [];
|
var wefaxImageHistory = [];
|
||||||
var WEFAX_MAX_IMAGES = 100;
|
var WEFAX_MAX_IMAGES = 100;
|
||||||
var wefaxLiveCtx = null;
|
var wefaxLiveCtx = null;
|
||||||
var wefaxLiveLineCount = 0;
|
var wefaxLiveLineCount = 0;
|
||||||
var wefaxLivePixelsPerLine = 1809;
|
var wefaxLivePixelsPerLine = 1809;
|
||||||
var wefaxActiveView = "live";
|
var wefaxActiveView = "live";
|
||||||
var wefaxFilterText = "";
|
var wefaxFilterText = "";
|
||||||
function currentWefaxHistoryRetentionMs() {
|
function currentWefaxHistoryRetentionMs() {
|
||||||
return window.getDecodeHistoryRetentionMs ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1e3;
|
||||||
}
|
}
|
||||||
function pruneWefaxHistory() {
|
function pruneWefaxHistory() {
|
||||||
var cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
||||||
wefaxImageHistory = wefaxImageHistory.filter(function(m) {
|
wefaxImageHistory = wefaxImageHistory.filter(function(m) {
|
||||||
return (m._tsMs || 0) > cutoff;
|
return (m._tsMs || 0) > cutoff;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
}
|
}
|
||||||
function scheduleWefaxUi(key, job) {
|
function scheduleWefaxUi(key, job) {
|
||||||
if (typeof window.trxScheduleUiFrameJob === "function") {
|
if (typeof wefaxWindow.trxScheduleUiFrameJob === "function") {
|
||||||
window.trxScheduleUiFrameJob(key, job);
|
wefaxWindow.trxScheduleUiFrameJob(key, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
job();
|
job();
|
||||||
}
|
}
|
||||||
function switchWefaxView(view) {
|
function switchWefaxView(view) {
|
||||||
wefaxActiveView = view;
|
wefaxActiveView = view;
|
||||||
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
|
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
|
||||||
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
|
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
|
||||||
@@ -52,37 +55,43 @@ function switchWefaxView(view) {
|
|||||||
if (view === "live" && wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.classList.add("sat-view-active");
|
if (view === "live" && wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.classList.add("sat-view-active");
|
||||||
if (view === "history" && wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.classList.add("sat-view-active");
|
if (view === "history" && wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.classList.add("sat-view-active");
|
||||||
if (view === "history") renderWefaxHistoryTable();
|
if (view === "history") renderWefaxHistoryTable();
|
||||||
}
|
}
|
||||||
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
|
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
|
||||||
switchWefaxView("live");
|
switchWefaxView("live");
|
||||||
});
|
});
|
||||||
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
|
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
|
||||||
switchWefaxView("history");
|
switchWefaxView("history");
|
||||||
});
|
});
|
||||||
function resetLiveCanvas(pixelsPerLine) {
|
function resetLiveCanvas(pixelsPerLine) {
|
||||||
|
const canvas = wefaxDom.liveCanvas;
|
||||||
|
if (!canvas) return;
|
||||||
wefaxLivePixelsPerLine = pixelsPerLine;
|
wefaxLivePixelsPerLine = pixelsPerLine;
|
||||||
wefaxLiveLineCount = 0;
|
wefaxLiveLineCount = 0;
|
||||||
wefaxDom.liveCanvas.width = pixelsPerLine;
|
canvas.width = pixelsPerLine;
|
||||||
wefaxDom.liveCanvas.height = 800;
|
canvas.height = 800;
|
||||||
wefaxLiveCtx = wefaxDom.liveCanvas.getContext("2d");
|
wefaxLiveCtx = canvas.getContext("2d");
|
||||||
|
if (!wefaxLiveCtx) return;
|
||||||
wefaxLiveCtx.fillStyle = "#000";
|
wefaxLiveCtx.fillStyle = "#000";
|
||||||
wefaxLiveCtx.fillRect(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
|
wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
|
||||||
}
|
}
|
||||||
function paintLine(lineBytes) {
|
function paintLine(lineBytes) {
|
||||||
|
const canvas = wefaxDom.liveCanvas;
|
||||||
|
if (!wefaxLiveCtx || !canvas) return;
|
||||||
|
const y = wefaxLiveLineCount;
|
||||||
|
if (y >= canvas.height) {
|
||||||
|
const old = wefaxLiveCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
canvas.height *= 2;
|
||||||
|
wefaxLiveCtx = canvas.getContext("2d");
|
||||||
if (!wefaxLiveCtx) return;
|
if (!wefaxLiveCtx) return;
|
||||||
var y = wefaxLiveLineCount;
|
|
||||||
if (y >= wefaxDom.liveCanvas.height) {
|
|
||||||
var old = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
|
|
||||||
wefaxDom.liveCanvas.height *= 2;
|
|
||||||
wefaxLiveCtx.putImageData(old, 0, 0);
|
wefaxLiveCtx.putImageData(old, 0, 0);
|
||||||
}
|
}
|
||||||
var w = wefaxLivePixelsPerLine;
|
const w = wefaxLivePixelsPerLine;
|
||||||
var imgData = wefaxLiveCtx.createImageData(w, 1);
|
const imgData = wefaxLiveCtx.createImageData(w, 1);
|
||||||
var d = imgData.data;
|
const d = imgData.data;
|
||||||
for (var x = 0; x < w; x++) {
|
for (let x = 0; x < w; x++) {
|
||||||
var v = x < lineBytes.length ? lineBytes[x] : 0;
|
const v = lineBytes[x] ?? 0;
|
||||||
var i = x * 4;
|
const i = x * 4;
|
||||||
d[i] = v;
|
d[i] = v;
|
||||||
d[i + 1] = v;
|
d[i + 1] = v;
|
||||||
d[i + 2] = v;
|
d[i + 2] = v;
|
||||||
@@ -90,24 +99,25 @@ function paintLine(lineBytes) {
|
|||||||
}
|
}
|
||||||
wefaxLiveCtx.putImageData(imgData, 0, y);
|
wefaxLiveCtx.putImageData(imgData, 0, y);
|
||||||
wefaxLiveLineCount++;
|
wefaxLiveLineCount++;
|
||||||
}
|
}
|
||||||
function renderWefaxLatestCard() {
|
function renderWefaxLatestCard() {
|
||||||
if (!wefaxDom.liveLatest) return;
|
if (!wefaxDom.liveLatest) return;
|
||||||
if (wefaxImageHistory.length === 0) {
|
if (wefaxImageHistory.length === 0) {
|
||||||
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var img = wefaxImageHistory[0];
|
const img = wefaxImageHistory[0];
|
||||||
var ts = img._ts || "--";
|
if (!img) return;
|
||||||
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
const ts = img._ts || "--";
|
||||||
var meta = [
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
||||||
img.ioc + " IOC",
|
const meta = [
|
||||||
img.lpm + " LPM",
|
`${String(img.ioc ?? "--")} IOC`,
|
||||||
img.line_count + " lines",
|
`${String(img.lpm ?? "--")} LPM`,
|
||||||
date + " " + ts
|
`${String(img.line_count ?? 0)} lines`,
|
||||||
|
`${date} ${ts}`
|
||||||
].join(" · ");
|
].join(" · ");
|
||||||
var imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
||||||
var html = '<div class="sat-latest-card">';
|
let html = '<div class="sat-latest-card">';
|
||||||
html += '<div class="sat-latest-title">Latest decoded image</div>';
|
html += '<div class="sat-latest-title">Latest decoded image</div>';
|
||||||
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + "</div>";
|
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + "</div>";
|
||||||
if (imgSrc) {
|
if (imgSrc) {
|
||||||
@@ -115,12 +125,12 @@ function renderWefaxLatestCard() {
|
|||||||
}
|
}
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
wefaxDom.liveLatest.innerHTML = html;
|
wefaxDom.liveLatest.innerHTML = html;
|
||||||
}
|
}
|
||||||
function getWefaxFilteredHistory() {
|
function getWefaxFilteredHistory() {
|
||||||
var items = wefaxImageHistory;
|
let items = wefaxImageHistory;
|
||||||
if (wefaxFilterText) {
|
if (wefaxFilterText) {
|
||||||
items = items.filter(function(i) {
|
items = items.filter(function(i) {
|
||||||
var haystack = [
|
const haystack = [
|
||||||
String(i.ioc || ""),
|
String(i.ioc || ""),
|
||||||
String(i.lpm || ""),
|
String(i.lpm || ""),
|
||||||
String(i.line_count || "")
|
String(i.line_count || "")
|
||||||
@@ -128,59 +138,62 @@ function getWefaxFilteredHistory() {
|
|||||||
return haystack.indexOf(wefaxFilterText) >= 0;
|
return haystack.indexOf(wefaxFilterText) >= 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest";
|
const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest";
|
||||||
if (sortVal === "oldest") items = items.slice().reverse();
|
if (sortVal === "oldest") items = items.slice().reverse();
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
function renderWefaxHistoryRow(img) {
|
function renderWefaxHistoryRow(img) {
|
||||||
var row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "sat-history-row";
|
row.className = "sat-history-row";
|
||||||
var ts = img._ts || "--";
|
const ts = img._ts || "--";
|
||||||
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
|
||||||
var ioc = img.ioc || "--";
|
const ioc = img.ioc || "--";
|
||||||
var lpm = img.lpm || "--";
|
const lpm = img.lpm || "--";
|
||||||
var lines = img.line_count || 0;
|
const lines = img.line_count || 0;
|
||||||
var imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
||||||
var link = imgSrc ? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>' : "--";
|
const link = imgSrc ? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>' : "--";
|
||||||
row.innerHTML = [
|
row.innerHTML = [
|
||||||
"<span>" + escapeHtml(date + " " + ts) + "</span>",
|
"<span>" + escapeHtml(date + " " + ts) + "</span>",
|
||||||
"<span>" + escapeHtml(String(ioc)) + "</span>",
|
"<span>" + escapeHtml(String(ioc)) + "</span>",
|
||||||
"<span>" + escapeHtml(String(lpm)) + "</span>",
|
"<span>" + escapeHtml(String(lpm)) + "</span>",
|
||||||
"<span>" + lines + "</span>",
|
`<span>${String(lines)}</span>`,
|
||||||
"<span>" + link + "</span>"
|
"<span>" + link + "</span>"
|
||||||
].join("");
|
].join("");
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
function renderWefaxHistoryTable() {
|
function renderWefaxHistoryTable() {
|
||||||
if (!wefaxDom.historyList) return;
|
if (!wefaxDom.historyList) return;
|
||||||
pruneWefaxHistory();
|
pruneWefaxHistory();
|
||||||
var items = getWefaxFilteredHistory();
|
const items = getWefaxFilteredHistory();
|
||||||
var fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (const item of items) {
|
||||||
fragment.appendChild(renderWefaxHistoryRow(items[i]));
|
fragment.appendChild(renderWefaxHistoryRow(item));
|
||||||
}
|
}
|
||||||
wefaxDom.historyList.replaceChildren(fragment);
|
wefaxDom.historyList.replaceChildren(fragment);
|
||||||
if (wefaxDom.historyCount) {
|
if (wefaxDom.historyCount) {
|
||||||
var total = wefaxImageHistory.length;
|
const total = wefaxImageHistory.length;
|
||||||
var shown = items.length;
|
const shown = items.length;
|
||||||
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? total + " image" + (total === 1 ? "" : "s") : shown + " of " + total + " images";
|
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function addWefaxImage(msg) {
|
function addWefaxImage(msg) {
|
||||||
var tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||||
msg._tsMs = tsMs;
|
msg._tsMs = tsMs;
|
||||||
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit"
|
second: "2-digit"
|
||||||
});
|
});
|
||||||
if (wefaxLiveCtx && wefaxLiveLineCount > 0) {
|
const canvas = wefaxDom.liveCanvas;
|
||||||
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxLiveLineCount);
|
if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
|
||||||
wefaxDom.liveCanvas.height = wefaxLiveLineCount;
|
const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
|
||||||
|
canvas.height = wefaxLiveLineCount;
|
||||||
|
wefaxLiveCtx = canvas.getContext("2d");
|
||||||
|
if (!wefaxLiveCtx) return;
|
||||||
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
||||||
try {
|
try {
|
||||||
msg._dataUrl = wefaxDom.liveCanvas.toDataURL("image/png");
|
msg._dataUrl = canvas.toDataURL("image/png");
|
||||||
} catch (e) {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wefaxImageHistory.unshift(msg);
|
wefaxImageHistory.unshift(msg);
|
||||||
@@ -191,8 +204,8 @@ function addWefaxImage(msg) {
|
|||||||
if (wefaxActiveView === "history") {
|
if (wefaxActiveView === "history") {
|
||||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.onServerWefaxProgress = function(msg) {
|
wefaxWindow.onServerWefaxProgress = function(msg) {
|
||||||
if (msg.state && !msg.line_data) {
|
if (msg.state && !msg.line_data) {
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
wefaxDom.status.textContent = msg.state;
|
wefaxDom.status.textContent = msg.state;
|
||||||
@@ -200,37 +213,37 @@ window.onServerWefaxProgress = function(msg) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.line_count <= 1 || !wefaxLiveCtx) {
|
if ((msg.line_count ?? 0) <= 1 || !wefaxLiveCtx) {
|
||||||
resetLiveCanvas(msg.pixels_per_line || 1809);
|
resetLiveCanvas(msg.pixels_per_line || 1809);
|
||||||
}
|
}
|
||||||
if (msg.line_data) {
|
if (msg.line_data) {
|
||||||
var binary = atob(msg.line_data);
|
const binary = atob(msg.line_data);
|
||||||
var bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
paintLine(bytes);
|
paintLine(bytes);
|
||||||
}
|
}
|
||||||
if (wefaxDom.liveInfo) {
|
if (wefaxDom.liveInfo) {
|
||||||
wefaxDom.liveInfo.textContent = "Line " + msg.line_count + " · " + msg.ioc + " IOC · " + msg.lpm + " LPM";
|
wefaxDom.liveInfo.textContent = `Line ${String(msg.line_count ?? 0)} · ${String(msg.ioc ?? "--")} IOC · ${String(msg.lpm ?? "--")} LPM`;
|
||||||
}
|
}
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
wefaxDom.status.textContent = "Receiving — line " + msg.line_count;
|
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
|
||||||
wefaxDom.status.style.color = "var(--text-accent)";
|
wefaxDom.status.style.color = "var(--text-accent)";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.onServerWefax = function(msg) {
|
wefaxWindow.onServerWefax = function(msg) {
|
||||||
addWefaxImage(msg);
|
addWefaxImage(msg);
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
wefaxDom.status.textContent = "Complete — " + msg.line_count + " lines";
|
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
|
||||||
wefaxDom.status.style.color = "";
|
wefaxDom.status.style.color = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.restoreWefaxHistory = function(messages) {
|
wefaxWindow.restoreWefaxHistory = function(messages) {
|
||||||
if (!messages || !messages.length) return;
|
if (!messages.length) return;
|
||||||
for (var i = 0; i < messages.length; i++) {
|
for (const message of messages) {
|
||||||
var tsMs = Number.isFinite(messages[i].ts_ms) ? Number(messages[i].ts_ms) : Date.now();
|
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
|
||||||
messages[i]._tsMs = tsMs;
|
message._tsMs = tsMs;
|
||||||
messages[i]._ts = new Date(tsMs).toLocaleTimeString([], {
|
message._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit"
|
second: "2-digit"
|
||||||
@@ -242,13 +255,13 @@ window.restoreWefaxHistory = function(messages) {
|
|||||||
if (wefaxActiveView === "history") {
|
if (wefaxActiveView === "history") {
|
||||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.pruneWefaxHistoryView = function() {
|
wefaxWindow.pruneWefaxHistoryView = function() {
|
||||||
pruneWefaxHistory();
|
pruneWefaxHistory();
|
||||||
renderWefaxHistoryTable();
|
renderWefaxHistoryTable();
|
||||||
renderWefaxLatestCard();
|
renderWefaxLatestCard();
|
||||||
};
|
};
|
||||||
window.resetWefaxHistoryView = function() {
|
wefaxWindow.resetWefaxHistoryView = function() {
|
||||||
wefaxImageHistory = [];
|
wefaxImageHistory = [];
|
||||||
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
|
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
||||||
@@ -260,45 +273,52 @@ window.resetWefaxHistoryView = function() {
|
|||||||
wefaxDom.status.textContent = "Idle";
|
wefaxDom.status.textContent = "Idle";
|
||||||
wefaxDom.status.style.color = "";
|
wefaxDom.status.style.color = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (wefaxDom.filterInput) {
|
if (wefaxDom.filterInput) {
|
||||||
|
const filterInput = wefaxDom.filterInput;
|
||||||
wefaxDom.filterInput.addEventListener("input", function() {
|
wefaxDom.filterInput.addEventListener("input", function() {
|
||||||
wefaxFilterText = wefaxDom.filterInput.value.trim().toUpperCase();
|
wefaxFilterText = filterInput.value.trim().toUpperCase();
|
||||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wefaxDom.sortSelect) {
|
if (wefaxDom.sortSelect) {
|
||||||
wefaxDom.sortSelect.addEventListener("change", function() {
|
wefaxDom.sortSelect.addEventListener("change", function() {
|
||||||
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.syncWefaxToggle = function(enabled) {
|
wefaxWindow.syncWefaxToggle = function(enabled) {
|
||||||
if (!wefaxDom.toggleBtn) return;
|
if (!wefaxDom.toggleBtn) return;
|
||||||
wefaxDom.toggleBtn.dataset.enabled = enabled ? "true" : "false";
|
wefaxDom.toggleBtn.dataset.enabled = enabled ? "true" : "false";
|
||||||
wefaxDom.toggleBtn.textContent = enabled ? "Disable WEFAX" : "Enable WEFAX";
|
wefaxDom.toggleBtn.textContent = enabled ? "Disable WEFAX" : "Enable WEFAX";
|
||||||
wefaxDom.toggleBtn.style.borderColor = enabled ? "#00d17f" : "";
|
wefaxDom.toggleBtn.style.borderColor = enabled ? "#00d17f" : "";
|
||||||
wefaxDom.toggleBtn.style.color = enabled ? "#00d17f" : "";
|
wefaxDom.toggleBtn.style.color = enabled ? "#00d17f" : "";
|
||||||
};
|
};
|
||||||
if (wefaxDom.toggleBtn) {
|
if (wefaxDom.toggleBtn) {
|
||||||
wefaxDom.toggleBtn.addEventListener("click", async function() {
|
const toggleButton = wefaxDom.toggleBtn;
|
||||||
|
wefaxDom.toggleBtn.addEventListener("click", () => {
|
||||||
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
if (window.takeSchedulerControlForDecoderDisable) {
|
if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
|
||||||
await window.takeSchedulerControlForDecoderDisable(wefaxDom.toggleBtn);
|
await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
|
||||||
}
|
}
|
||||||
await postPath("/toggle_wefax_decode");
|
await wefaxWindow.postPath?.("/toggle_wefax_decode");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WEFAX toggle failed", e);
|
console.error("WEFAX toggle failed", e);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wefaxDom.clearBtn) {
|
if (wefaxDom.clearBtn) {
|
||||||
wefaxDom.clearBtn.addEventListener("click", async function() {
|
wefaxDom.clearBtn.addEventListener("click", () => {
|
||||||
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await postPath("/clear_wefax_decode");
|
await wefaxWindow.postPath?.("/clear_wefax_decode");
|
||||||
window.resetWefaxHistoryView();
|
wefaxWindow.resetWefaxHistoryView?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WEFAX clear failed", e);
|
console.error("WEFAX clear failed", e);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
renderWefaxLatestCard();
|
renderWefaxLatestCard();
|
||||||
|
})();
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ await build({
|
|||||||
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
|
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
|
||||||
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
||||||
vchan: path.join(sourceDir, "plugins", "vchan.js"),
|
vchan: path.join(sourceDir, "plugins", "vchan.js"),
|
||||||
wefax: path.join(sourceDir, "plugins", "wefax.js"),
|
|
||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
bundle: false,
|
bundle: false,
|
||||||
@@ -51,6 +50,7 @@ await build({
|
|||||||
ft8: path.join(sourceDir, "plugins", "ft8.ts"),
|
ft8: path.join(sourceDir, "plugins", "ft8.ts"),
|
||||||
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"),
|
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"),
|
||||||
vdes: path.join(sourceDir, "plugins", "vdes.ts"),
|
vdes: path.join(sourceDir, "plugins", "vdes.ts"),
|
||||||
|
wefax: path.join(sourceDir, "plugins", "wefax.ts"),
|
||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
|
|||||||
|
|
||||||
const loaded = new Set<string>();
|
const loaded = new Set<string>();
|
||||||
const loading = new Map<string, Promise<void>>();
|
const loading = new Map<string, Promise<void>>();
|
||||||
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]);
|
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]);
|
||||||
|
|
||||||
function loadLegacyScript(path: string): Promise<void> {
|
function loadLegacyScript(path: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
+150
-101
@@ -2,6 +2,43 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
||||||
|
interface WefaxImage {
|
||||||
|
ts_ms?: number;
|
||||||
|
ioc?: number;
|
||||||
|
lpm?: number;
|
||||||
|
line_count?: number;
|
||||||
|
path?: string;
|
||||||
|
_tsMs?: number;
|
||||||
|
_ts?: string;
|
||||||
|
_dataUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WefaxProgress {
|
||||||
|
state?: string;
|
||||||
|
line_data?: string;
|
||||||
|
line_count?: number;
|
||||||
|
pixels_per_line?: number;
|
||||||
|
ioc?: number;
|
||||||
|
lpm?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WefaxBridge {
|
||||||
|
getDecodeHistoryRetentionMs?: () => number;
|
||||||
|
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
||||||
|
postPath?: (path: string) => Promise<unknown>;
|
||||||
|
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
||||||
|
onServerWefaxProgress?: (message: WefaxProgress) => void;
|
||||||
|
onServerWefax?: (message: WefaxImage) => void;
|
||||||
|
restoreWefaxHistory?: (messages: WefaxImage[]) => void;
|
||||||
|
pruneWefaxHistoryView?: () => void;
|
||||||
|
resetWefaxHistoryView?: () => void;
|
||||||
|
syncWefaxToggle?: (enabled: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wefaxWindow = window as unknown as WefaxBridge;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// wefax.js — WEFAX decoder plugin for trx-frontend-http
|
// wefax.js — WEFAX decoder plugin for trx-frontend-http
|
||||||
// Live view: decoder state, live canvas, latest image card
|
// Live view: decoder state, live canvas, latest image card
|
||||||
@@ -9,18 +46,18 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// ── DOM references (cached once) ───────────────────────────────────
|
// ── DOM references (cached once) ───────────────────────────────────
|
||||||
var wefaxDom = {
|
const wefaxDom = {
|
||||||
status: document.getElementById('wefax-status'),
|
status: document.getElementById('wefax-status'),
|
||||||
liveView: document.getElementById('wefax-live-view'),
|
liveView: document.getElementById('wefax-live-view'),
|
||||||
historyView: document.getElementById('wefax-history-view'),
|
historyView: document.getElementById('wefax-history-view'),
|
||||||
liveContainer: document.getElementById('wefax-live-container'),
|
liveContainer: document.getElementById('wefax-live-container'),
|
||||||
liveInfo: document.getElementById('wefax-live-info'),
|
liveInfo: document.getElementById('wefax-live-info'),
|
||||||
liveCanvas: document.getElementById('wefax-live-canvas'),
|
liveCanvas: document.getElementById('wefax-live-canvas') as HTMLCanvasElement | null,
|
||||||
liveLatest: document.getElementById('wefax-live-latest'),
|
liveLatest: document.getElementById('wefax-live-latest'),
|
||||||
historyList: document.getElementById('wefax-history-list'),
|
historyList: document.getElementById('wefax-history-list'),
|
||||||
historyCount: document.getElementById('wefax-history-count'),
|
historyCount: document.getElementById('wefax-history-count'),
|
||||||
filterInput: document.getElementById('wefax-filter'),
|
filterInput: document.getElementById('wefax-filter') as HTMLInputElement | null,
|
||||||
sortSelect: document.getElementById('wefax-sort'),
|
sortSelect: document.getElementById('wefax-sort') as HTMLSelectElement | null,
|
||||||
toggleBtn: document.getElementById('wefax-decode-toggle-btn'),
|
toggleBtn: document.getElementById('wefax-decode-toggle-btn'),
|
||||||
clearBtn: document.getElementById('wefax-clear-btn'),
|
clearBtn: document.getElementById('wefax-clear-btn'),
|
||||||
viewLiveBtn: document.getElementById('wefax-view-live'),
|
viewLiveBtn: document.getElementById('wefax-view-live'),
|
||||||
@@ -28,25 +65,25 @@ var wefaxDom = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── State ───────────────────────────────────────────────────────────
|
// ── State ───────────────────────────────────────────────────────────
|
||||||
var wefaxImageHistory = [];
|
let wefaxImageHistory: WefaxImage[] = [];
|
||||||
var WEFAX_MAX_IMAGES = 100;
|
const WEFAX_MAX_IMAGES = 100;
|
||||||
var wefaxLiveCtx = null;
|
let wefaxLiveCtx: CanvasRenderingContext2D | null = null;
|
||||||
var wefaxLiveLineCount = 0;
|
let wefaxLiveLineCount = 0;
|
||||||
var wefaxLivePixelsPerLine = 1809;
|
let wefaxLivePixelsPerLine = 1809;
|
||||||
var wefaxActiveView = 'live';
|
let wefaxActiveView: 'live' | 'history' = 'live';
|
||||||
var wefaxFilterText = '';
|
let wefaxFilterText = '';
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────────────────
|
||||||
function currentWefaxHistoryRetentionMs() {
|
function currentWefaxHistoryRetentionMs(): number {
|
||||||
return window.getDecodeHistoryRetentionMs ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000;
|
return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pruneWefaxHistory() {
|
function pruneWefaxHistory() {
|
||||||
var cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
||||||
wefaxImageHistory = wefaxImageHistory.filter(function (m) { return (m._tsMs || 0) > cutoff; });
|
wefaxImageHistory = wefaxImageHistory.filter(function (m) { return (m._tsMs || 0) > cutoff; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s: unknown): string {
|
||||||
return String(s)
|
return String(s)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@@ -54,16 +91,16 @@ function escapeHtml(s) {
|
|||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleWefaxUi(key, job) {
|
function scheduleWefaxUi(key: string, job: () => void): void {
|
||||||
if (typeof window.trxScheduleUiFrameJob === 'function') {
|
if (typeof wefaxWindow.trxScheduleUiFrameJob === 'function') {
|
||||||
window.trxScheduleUiFrameJob(key, job);
|
wefaxWindow.trxScheduleUiFrameJob(key, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
job();
|
job();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── View switching ──────────────────────────────────────────────────
|
// ── View switching ──────────────────────────────────────────────────
|
||||||
function switchWefaxView(view) {
|
function switchWefaxView(view: 'live' | 'history'): void {
|
||||||
wefaxActiveView = view;
|
wefaxActiveView = view;
|
||||||
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === 'live' ? '' : 'none';
|
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === 'live' ? '' : 'none';
|
||||||
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === 'history' ? '' : 'none';
|
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === 'history' ? '' : 'none';
|
||||||
@@ -81,33 +118,39 @@ if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener('click', func
|
|||||||
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener('click', function () { switchWefaxView('history'); });
|
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener('click', function () { switchWefaxView('history'); });
|
||||||
|
|
||||||
// ── Live canvas rendering ───────────────────────────────────────────
|
// ── Live canvas rendering ───────────────────────────────────────────
|
||||||
function resetLiveCanvas(pixelsPerLine) {
|
function resetLiveCanvas(pixelsPerLine: number): void {
|
||||||
|
const canvas = wefaxDom.liveCanvas;
|
||||||
|
if (!canvas) return;
|
||||||
wefaxLivePixelsPerLine = pixelsPerLine;
|
wefaxLivePixelsPerLine = pixelsPerLine;
|
||||||
wefaxLiveLineCount = 0;
|
wefaxLiveLineCount = 0;
|
||||||
wefaxDom.liveCanvas.width = pixelsPerLine;
|
canvas.width = pixelsPerLine;
|
||||||
wefaxDom.liveCanvas.height = 800;
|
canvas.height = 800;
|
||||||
wefaxLiveCtx = wefaxDom.liveCanvas.getContext('2d');
|
wefaxLiveCtx = canvas.getContext('2d');
|
||||||
|
if (!wefaxLiveCtx) return;
|
||||||
wefaxLiveCtx.fillStyle = '#000';
|
wefaxLiveCtx.fillStyle = '#000';
|
||||||
wefaxLiveCtx.fillRect(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
|
wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = '';
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintLine(lineBytes) {
|
function paintLine(lineBytes: Uint8Array): void {
|
||||||
if (!wefaxLiveCtx) return;
|
const canvas = wefaxDom.liveCanvas;
|
||||||
var y = wefaxLiveLineCount;
|
if (!wefaxLiveCtx || !canvas) return;
|
||||||
|
const y = wefaxLiveLineCount;
|
||||||
|
|
||||||
if (y >= wefaxDom.liveCanvas.height) {
|
if (y >= canvas.height) {
|
||||||
var old = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
|
const old = wefaxLiveCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
wefaxDom.liveCanvas.height *= 2;
|
canvas.height *= 2;
|
||||||
|
wefaxLiveCtx = canvas.getContext('2d');
|
||||||
|
if (!wefaxLiveCtx) return;
|
||||||
wefaxLiveCtx.putImageData(old, 0, 0);
|
wefaxLiveCtx.putImageData(old, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var w = wefaxLivePixelsPerLine;
|
const w = wefaxLivePixelsPerLine;
|
||||||
var imgData = wefaxLiveCtx.createImageData(w, 1);
|
const imgData = wefaxLiveCtx.createImageData(w, 1);
|
||||||
var d = imgData.data;
|
const d = imgData.data;
|
||||||
for (var x = 0; x < w; x++) {
|
for (let x = 0; x < w; x++) {
|
||||||
var v = x < lineBytes.length ? lineBytes[x] : 0;
|
const v = lineBytes[x] ?? 0;
|
||||||
var i = x * 4;
|
const i = x * 4;
|
||||||
d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
|
d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
|
||||||
}
|
}
|
||||||
wefaxLiveCtx.putImageData(imgData, 0, y);
|
wefaxLiveCtx.putImageData(imgData, 0, y);
|
||||||
@@ -123,23 +166,24 @@ function renderWefaxLatestCard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var img = wefaxImageHistory[0];
|
const img = wefaxImageHistory[0];
|
||||||
var ts = img._ts || '--';
|
if (!img) return;
|
||||||
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
|
const ts = img._ts || '--';
|
||||||
var meta = [
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
|
||||||
img.ioc + ' IOC',
|
const meta = [
|
||||||
img.lpm + ' LPM',
|
`${String(img.ioc ?? '--')} IOC`,
|
||||||
img.line_count + ' lines',
|
`${String(img.lpm ?? '--')} LPM`,
|
||||||
date + ' ' + ts,
|
`${String(img.line_count ?? 0)} lines`,
|
||||||
|
`${date} ${ts}`,
|
||||||
].join(' \u00b7 ');
|
].join(' \u00b7 ');
|
||||||
|
|
||||||
var imgSrc = img._dataUrl
|
const imgSrc = img._dataUrl
|
||||||
? img._dataUrl
|
? img._dataUrl
|
||||||
: img.path
|
: img.path
|
||||||
? '/images/' + escapeHtml(img.path.split('/').pop())
|
? '/images/' + escapeHtml(img.path.split('/').pop())
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var html = '<div class="sat-latest-card">';
|
let html = '<div class="sat-latest-card">';
|
||||||
html += '<div class="sat-latest-title">Latest decoded image</div>';
|
html += '<div class="sat-latest-title">Latest decoded image</div>';
|
||||||
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + '</div>';
|
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + '</div>';
|
||||||
if (imgSrc) {
|
if (imgSrc) {
|
||||||
@@ -151,11 +195,11 @@ function renderWefaxLatestCard() {
|
|||||||
|
|
||||||
// ── History view: table ─────────────────────────────────────────────
|
// ── History view: table ─────────────────────────────────────────────
|
||||||
function getWefaxFilteredHistory() {
|
function getWefaxFilteredHistory() {
|
||||||
var items = wefaxImageHistory;
|
let items = wefaxImageHistory;
|
||||||
|
|
||||||
if (wefaxFilterText) {
|
if (wefaxFilterText) {
|
||||||
items = items.filter(function (i) {
|
items = items.filter(function (i) {
|
||||||
var haystack = [
|
const haystack = [
|
||||||
String(i.ioc || ''),
|
String(i.ioc || ''),
|
||||||
String(i.lpm || ''),
|
String(i.lpm || ''),
|
||||||
String(i.line_count || ''),
|
String(i.line_count || ''),
|
||||||
@@ -164,28 +208,28 @@ function getWefaxFilteredHistory() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : 'newest';
|
const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : 'newest';
|
||||||
if (sortVal === 'oldest') items = items.slice().reverse();
|
if (sortVal === 'oldest') items = items.slice().reverse();
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWefaxHistoryRow(img) {
|
function renderWefaxHistoryRow(img: WefaxImage): HTMLElement {
|
||||||
var row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'sat-history-row';
|
row.className = 'sat-history-row';
|
||||||
|
|
||||||
var ts = img._ts || '--';
|
const ts = img._ts || '--';
|
||||||
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
|
||||||
var ioc = img.ioc || '--';
|
const ioc = img.ioc || '--';
|
||||||
var lpm = img.lpm || '--';
|
const lpm = img.lpm || '--';
|
||||||
var lines = img.line_count || 0;
|
const lines = img.line_count || 0;
|
||||||
|
|
||||||
var imgSrc = img._dataUrl
|
const imgSrc = img._dataUrl
|
||||||
? img._dataUrl
|
? img._dataUrl
|
||||||
: img.path
|
: img.path
|
||||||
? '/images/' + escapeHtml(img.path.split('/').pop())
|
? '/images/' + escapeHtml(img.path.split('/').pop())
|
||||||
: null;
|
: null;
|
||||||
var link = imgSrc
|
const link = imgSrc
|
||||||
? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>'
|
? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>'
|
||||||
: '--';
|
: '--';
|
||||||
|
|
||||||
@@ -193,7 +237,7 @@ function renderWefaxHistoryRow(img) {
|
|||||||
'<span>' + escapeHtml(date + ' ' + ts) + '</span>',
|
'<span>' + escapeHtml(date + ' ' + ts) + '</span>',
|
||||||
'<span>' + escapeHtml(String(ioc)) + '</span>',
|
'<span>' + escapeHtml(String(ioc)) + '</span>',
|
||||||
'<span>' + escapeHtml(String(lpm)) + '</span>',
|
'<span>' + escapeHtml(String(lpm)) + '</span>',
|
||||||
'<span>' + lines + '</span>',
|
`<span>${String(lines)}</span>`,
|
||||||
'<span>' + link + '</span>',
|
'<span>' + link + '</span>',
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
@@ -203,28 +247,28 @@ function renderWefaxHistoryRow(img) {
|
|||||||
function renderWefaxHistoryTable() {
|
function renderWefaxHistoryTable() {
|
||||||
if (!wefaxDom.historyList) return;
|
if (!wefaxDom.historyList) return;
|
||||||
pruneWefaxHistory();
|
pruneWefaxHistory();
|
||||||
var items = getWefaxFilteredHistory();
|
const items = getWefaxFilteredHistory();
|
||||||
var fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (const item of items) {
|
||||||
fragment.appendChild(renderWefaxHistoryRow(items[i]));
|
fragment.appendChild(renderWefaxHistoryRow(item));
|
||||||
}
|
}
|
||||||
wefaxDom.historyList.replaceChildren(fragment);
|
wefaxDom.historyList.replaceChildren(fragment);
|
||||||
|
|
||||||
if (wefaxDom.historyCount) {
|
if (wefaxDom.historyCount) {
|
||||||
var total = wefaxImageHistory.length;
|
const total = wefaxImageHistory.length;
|
||||||
var shown = items.length;
|
const shown = items.length;
|
||||||
wefaxDom.historyCount.textContent =
|
wefaxDom.historyCount.textContent =
|
||||||
total === 0
|
total === 0
|
||||||
? 'No images yet'
|
? 'No images yet'
|
||||||
: shown === total
|
: shown === total
|
||||||
? total + ' image' + (total === 1 ? '' : 's')
|
? `${String(total)} image${total === 1 ? '' : 's'}`
|
||||||
: shown + ' of ' + total + ' images';
|
: `${String(shown)} of ${String(total)} images`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Add image to history ────────────────────────────────────────────
|
// ── Add image to history ────────────────────────────────────────────
|
||||||
function addWefaxImage(msg) {
|
function addWefaxImage(msg: WefaxImage): void {
|
||||||
var tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
||||||
msg._tsMs = tsMs;
|
msg._tsMs = tsMs;
|
||||||
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
@@ -233,11 +277,14 @@ function addWefaxImage(msg) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Capture the live canvas as a data URI for thumbnails.
|
// Capture the live canvas as a data URI for thumbnails.
|
||||||
if (wefaxLiveCtx && wefaxLiveLineCount > 0) {
|
const canvas = wefaxDom.liveCanvas;
|
||||||
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxLiveLineCount);
|
if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
|
||||||
wefaxDom.liveCanvas.height = wefaxLiveLineCount;
|
const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
|
||||||
|
canvas.height = wefaxLiveLineCount;
|
||||||
|
wefaxLiveCtx = canvas.getContext('2d');
|
||||||
|
if (!wefaxLiveCtx) return;
|
||||||
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
||||||
try { msg._dataUrl = wefaxDom.liveCanvas.toDataURL('image/png'); } catch (e) {}
|
try { msg._dataUrl = canvas.toDataURL('image/png'); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
wefaxImageHistory.unshift(msg);
|
wefaxImageHistory.unshift(msg);
|
||||||
@@ -252,7 +299,7 @@ function addWefaxImage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── SSE event handlers (public API) ─────────────────────────────────
|
// ── SSE event handlers (public API) ─────────────────────────────────
|
||||||
window.onServerWefaxProgress = function (msg) {
|
wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) {
|
||||||
// State-only update (no image data): show decoder state in status.
|
// State-only update (no image data): show decoder state in status.
|
||||||
if (msg.state && !msg.line_data) {
|
if (msg.state && !msg.line_data) {
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
@@ -263,43 +310,43 @@ window.onServerWefaxProgress = function (msg) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.line_count <= 1 || !wefaxLiveCtx) {
|
if ((msg.line_count ?? 0) <= 1 || !wefaxLiveCtx) {
|
||||||
resetLiveCanvas(msg.pixels_per_line || 1809);
|
resetLiveCanvas(msg.pixels_per_line || 1809);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.line_data) {
|
if (msg.line_data) {
|
||||||
var binary = atob(msg.line_data);
|
const binary = atob(msg.line_data);
|
||||||
var bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
paintLine(bytes);
|
paintLine(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wefaxDom.liveInfo) {
|
if (wefaxDom.liveInfo) {
|
||||||
wefaxDom.liveInfo.textContent =
|
wefaxDom.liveInfo.textContent =
|
||||||
'Line ' + msg.line_count + ' \u00b7 ' + msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM';
|
`Line ${String(msg.line_count ?? 0)} \u00b7 ${String(msg.ioc ?? '--')} IOC \u00b7 ${String(msg.lpm ?? '--')} LPM`;
|
||||||
}
|
}
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
wefaxDom.status.textContent = 'Receiving \u2014 line ' + msg.line_count;
|
wefaxDom.status.textContent = `Receiving \u2014 line ${String(msg.line_count ?? 0)}`;
|
||||||
wefaxDom.status.style.color = 'var(--text-accent)';
|
wefaxDom.status.style.color = 'var(--text-accent)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onServerWefax = function (msg) {
|
wefaxWindow.onServerWefax = function (msg: WefaxImage) {
|
||||||
addWefaxImage(msg);
|
addWefaxImage(msg);
|
||||||
|
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
||||||
if (wefaxDom.status) {
|
if (wefaxDom.status) {
|
||||||
wefaxDom.status.textContent = 'Complete \u2014 ' + msg.line_count + ' lines';
|
wefaxDom.status.textContent = `Complete \u2014 ${String(msg.line_count ?? 0)} lines`;
|
||||||
wefaxDom.status.style.color = '';
|
wefaxDom.status.style.color = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.restoreWefaxHistory = function (messages) {
|
wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) {
|
||||||
if (!messages || !messages.length) return;
|
if (!messages.length) return;
|
||||||
for (var i = 0; i < messages.length; i++) {
|
for (const message of messages) {
|
||||||
var tsMs = Number.isFinite(messages[i].ts_ms) ? Number(messages[i].ts_ms) : Date.now();
|
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
|
||||||
messages[i]._tsMs = tsMs;
|
message._tsMs = tsMs;
|
||||||
messages[i]._ts = new Date(tsMs).toLocaleTimeString([], {
|
message._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
@@ -313,13 +360,13 @@ window.restoreWefaxHistory = function (messages) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.pruneWefaxHistoryView = function () {
|
wefaxWindow.pruneWefaxHistoryView = function () {
|
||||||
pruneWefaxHistory();
|
pruneWefaxHistory();
|
||||||
renderWefaxHistoryTable();
|
renderWefaxHistoryTable();
|
||||||
renderWefaxLatestCard();
|
renderWefaxLatestCard();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.resetWefaxHistoryView = function () {
|
wefaxWindow.resetWefaxHistoryView = function () {
|
||||||
wefaxImageHistory = [];
|
wefaxImageHistory = [];
|
||||||
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = '';
|
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = '';
|
||||||
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
|
||||||
@@ -335,8 +382,9 @@ window.resetWefaxHistoryView = function () {
|
|||||||
|
|
||||||
// ── Filter / sort handlers ──────────────────────────────────────────
|
// ── Filter / sort handlers ──────────────────────────────────────────
|
||||||
if (wefaxDom.filterInput) {
|
if (wefaxDom.filterInput) {
|
||||||
|
const filterInput = wefaxDom.filterInput;
|
||||||
wefaxDom.filterInput.addEventListener('input', function () {
|
wefaxDom.filterInput.addEventListener('input', function () {
|
||||||
wefaxFilterText = wefaxDom.filterInput.value.trim().toUpperCase();
|
wefaxFilterText = filterInput.value.trim().toUpperCase();
|
||||||
scheduleWefaxUi('wefax-history', renderWefaxHistoryTable);
|
scheduleWefaxUi('wefax-history', renderWefaxHistoryTable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -350,7 +398,7 @@ if (wefaxDom.sortSelect) {
|
|||||||
// Sync the Enable/Disable button from the SSE state update. This is
|
// Sync the Enable/Disable button from the SSE state update. This is
|
||||||
// belt-and-suspenders alongside app.js _decoderToggles — guarantees the
|
// belt-and-suspenders alongside app.js _decoderToggles — guarantees the
|
||||||
// WEFAX button always reflects the server state.
|
// WEFAX button always reflects the server state.
|
||||||
window.syncWefaxToggle = function (enabled) {
|
wefaxWindow.syncWefaxToggle = function (enabled: boolean) {
|
||||||
if (!wefaxDom.toggleBtn) return;
|
if (!wefaxDom.toggleBtn) return;
|
||||||
wefaxDom.toggleBtn.dataset.enabled = enabled ? 'true' : 'false';
|
wefaxDom.toggleBtn.dataset.enabled = enabled ? 'true' : 'false';
|
||||||
wefaxDom.toggleBtn.textContent = enabled ? 'Disable WEFAX' : 'Enable WEFAX';
|
wefaxDom.toggleBtn.textContent = enabled ? 'Disable WEFAX' : 'Enable WEFAX';
|
||||||
@@ -360,26 +408,27 @@ window.syncWefaxToggle = function (enabled) {
|
|||||||
|
|
||||||
// ── Button handlers ─────────────────────────────────────────────────
|
// ── Button handlers ─────────────────────────────────────────────────
|
||||||
if (wefaxDom.toggleBtn) {
|
if (wefaxDom.toggleBtn) {
|
||||||
wefaxDom.toggleBtn.addEventListener('click', async function () {
|
const toggleButton = wefaxDom.toggleBtn;
|
||||||
|
wefaxDom.toggleBtn.addEventListener('click', () => { void (async () => {
|
||||||
try {
|
try {
|
||||||
if (window.takeSchedulerControlForDecoderDisable) {
|
if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
|
||||||
await window.takeSchedulerControlForDecoderDisable(wefaxDom.toggleBtn);
|
await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
|
||||||
}
|
}
|
||||||
await postPath('/toggle_wefax_decode');
|
await wefaxWindow.postPath?.('/toggle_wefax_decode');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('WEFAX toggle failed', e);
|
console.error('WEFAX toggle failed', e);
|
||||||
}
|
}
|
||||||
});
|
})(); });
|
||||||
}
|
}
|
||||||
if (wefaxDom.clearBtn) {
|
if (wefaxDom.clearBtn) {
|
||||||
wefaxDom.clearBtn.addEventListener('click', async function () {
|
wefaxDom.clearBtn.addEventListener('click', () => { void (async () => {
|
||||||
try {
|
try {
|
||||||
await postPath('/clear_wefax_decode');
|
await wefaxWindow.postPath?.('/clear_wefax_decode');
|
||||||
window.resetWefaxHistoryView();
|
wefaxWindow.resetWefaxHistoryView?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('WEFAX clear failed', e);
|
console.error('WEFAX clear failed', e);
|
||||||
}
|
}
|
||||||
});
|
})(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Initial render ──────────────────────────────────────────────────
|
// ── Initial render ──────────────────────────────────────────────────
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import test from "node:test";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
test("WEFAX entry exposes typed lifecycle handlers and renders decoder state", async () => {
|
||||||
|
const status = { textContent: "", style: { color: "" } };
|
||||||
|
const window = {};
|
||||||
|
const context = vm.createContext({
|
||||||
|
window,
|
||||||
|
document: { getElementById: (id) => id === "wefax-status" ? status : null },
|
||||||
|
Date,
|
||||||
|
Number,
|
||||||
|
String,
|
||||||
|
Uint8Array,
|
||||||
|
console,
|
||||||
|
});
|
||||||
|
const source = await readFile(new URL("../../assets/web/generated/wefax.js", import.meta.url), "utf8");
|
||||||
|
new vm.Script(source).runInContext(context);
|
||||||
|
|
||||||
|
assert.equal(typeof window.onServerWefaxProgress, "function");
|
||||||
|
assert.equal(typeof window.restoreWefaxHistory, "function");
|
||||||
|
window.onServerWefaxProgress({ state: "Scanning 12 MHz" });
|
||||||
|
assert.equal(status.textContent, "Scanning 12 MHz");
|
||||||
|
assert.equal(status.style.color, "var(--text-accent)");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user