333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
// src/plugins/wefax.ts
|
|
var wefaxWindow = window;
|
|
var wefaxDom = {
|
|
status: document.getElementById("wefax-status"),
|
|
liveView: document.getElementById("wefax-live-view"),
|
|
historyView: document.getElementById("wefax-history-view"),
|
|
liveContainer: document.getElementById("wefax-live-container"),
|
|
liveInfo: document.getElementById("wefax-live-info"),
|
|
liveCanvas: document.getElementById("wefax-live-canvas"),
|
|
liveLatest: document.getElementById("wefax-live-latest"),
|
|
historyList: document.getElementById("wefax-history-list"),
|
|
historyCount: document.getElementById("wefax-history-count"),
|
|
filterInput: document.getElementById("wefax-filter"),
|
|
sortSelect: document.getElementById("wefax-sort"),
|
|
toggleBtn: document.getElementById("wefax-decode-toggle-btn"),
|
|
clearBtn: document.getElementById("wefax-clear-btn"),
|
|
viewLiveBtn: document.getElementById("wefax-view-live"),
|
|
viewHistoryBtn: document.getElementById("wefax-view-history")
|
|
};
|
|
var wefaxImageHistory = [];
|
|
var WEFAX_MAX_IMAGES = 100;
|
|
var wefaxLiveCtx = null;
|
|
var wefaxLiveLineCount = 0;
|
|
var wefaxLivePixelsPerLine = 1809;
|
|
var wefaxActiveView = "live";
|
|
var wefaxFilterText = "";
|
|
function currentWefaxHistoryRetentionMs() {
|
|
return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1e3;
|
|
}
|
|
function pruneWefaxHistory() {
|
|
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
|
|
wefaxImageHistory = wefaxImageHistory.filter(function(m) {
|
|
return (m._tsMs || 0) > cutoff;
|
|
});
|
|
}
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
function scheduleWefaxUi(key, job) {
|
|
if (typeof wefaxWindow.trxScheduleUiFrameJob === "function") {
|
|
wefaxWindow.trxScheduleUiFrameJob(key, job);
|
|
return;
|
|
}
|
|
job();
|
|
}
|
|
function switchWefaxView(view) {
|
|
wefaxActiveView = view;
|
|
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
|
|
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
|
|
[wefaxDom.viewLiveBtn, wefaxDom.viewHistoryBtn].forEach(function(btn) {
|
|
if (btn) btn.classList.remove("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") renderWefaxHistoryTable();
|
|
}
|
|
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
|
|
switchWefaxView("live");
|
|
});
|
|
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
|
|
switchWefaxView("history");
|
|
});
|
|
function resetLiveCanvas(pixelsPerLine) {
|
|
const canvas = wefaxDom.liveCanvas;
|
|
if (!canvas) return;
|
|
wefaxLivePixelsPerLine = pixelsPerLine;
|
|
wefaxLiveLineCount = 0;
|
|
canvas.width = pixelsPerLine;
|
|
canvas.height = 800;
|
|
wefaxLiveCtx = canvas.getContext("2d");
|
|
if (!wefaxLiveCtx) return;
|
|
wefaxLiveCtx.fillStyle = "#000";
|
|
wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
|
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
|
|
}
|
|
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;
|
|
wefaxLiveCtx.putImageData(old, 0, 0);
|
|
}
|
|
const w = wefaxLivePixelsPerLine;
|
|
const imgData = wefaxLiveCtx.createImageData(w, 1);
|
|
const d = imgData.data;
|
|
for (let x = 0; x < w; x++) {
|
|
const v = lineBytes[x] ?? 0;
|
|
const i = x * 4;
|
|
d[i] = v;
|
|
d[i + 1] = v;
|
|
d[i + 2] = v;
|
|
d[i + 3] = 255;
|
|
}
|
|
wefaxLiveCtx.putImageData(imgData, 0, y);
|
|
wefaxLiveLineCount++;
|
|
}
|
|
function renderWefaxLatestCard() {
|
|
if (!wefaxDom.liveLatest) return;
|
|
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>';
|
|
return;
|
|
}
|
|
const img = wefaxImageHistory[0];
|
|
if (!img) return;
|
|
const ts = img._ts || "--";
|
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
|
const meta = [
|
|
`${String(img.ioc ?? "--")} IOC`,
|
|
`${String(img.lpm ?? "--")} LPM`,
|
|
`${String(img.line_count ?? 0)} lines`,
|
|
`${date} ${ts}`
|
|
].join(" · ");
|
|
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
|
let html = '<div class="sat-latest-card">';
|
|
html += '<div class="sat-latest-title">Latest decoded image</div>';
|
|
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + "</div>";
|
|
if (imgSrc) {
|
|
html += '<a href="' + imgSrc + '" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">View full image</a>';
|
|
}
|
|
html += "</div>";
|
|
wefaxDom.liveLatest.innerHTML = html;
|
|
}
|
|
function getWefaxFilteredHistory() {
|
|
let items = wefaxImageHistory;
|
|
if (wefaxFilterText) {
|
|
items = items.filter(function(i) {
|
|
const haystack = [
|
|
String(i.ioc || ""),
|
|
String(i.lpm || ""),
|
|
String(i.line_count || "")
|
|
].join(" ").toUpperCase();
|
|
return haystack.indexOf(wefaxFilterText) >= 0;
|
|
});
|
|
}
|
|
const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest";
|
|
if (sortVal === "oldest") items = items.slice().reverse();
|
|
return items;
|
|
}
|
|
function renderWefaxHistoryRow(img) {
|
|
const row = document.createElement("div");
|
|
row.className = "sat-history-row";
|
|
const ts = img._ts || "--";
|
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
|
|
const ioc = img.ioc || "--";
|
|
const lpm = img.lpm || "--";
|
|
const lines = img.line_count || 0;
|
|
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
|
|
const link = imgSrc ? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>' : "--";
|
|
row.innerHTML = [
|
|
"<span>" + escapeHtml(date + " " + ts) + "</span>",
|
|
"<span>" + escapeHtml(String(ioc)) + "</span>",
|
|
"<span>" + escapeHtml(String(lpm)) + "</span>",
|
|
`<span>${String(lines)}</span>`,
|
|
"<span>" + link + "</span>"
|
|
].join("");
|
|
return row;
|
|
}
|
|
function renderWefaxHistoryTable() {
|
|
if (!wefaxDom.historyList) return;
|
|
pruneWefaxHistory();
|
|
const items = getWefaxFilteredHistory();
|
|
const fragment = document.createDocumentFragment();
|
|
for (const item of items) {
|
|
fragment.appendChild(renderWefaxHistoryRow(item));
|
|
}
|
|
wefaxDom.historyList.replaceChildren(fragment);
|
|
if (wefaxDom.historyCount) {
|
|
const total = wefaxImageHistory.length;
|
|
const shown = items.length;
|
|
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) {
|
|
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
|
|
msg._tsMs = tsMs;
|
|
msg._ts = new Date(tsMs).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
const canvas = wefaxDom.liveCanvas;
|
|
if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
|
|
const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
|
|
canvas.height = wefaxLiveLineCount;
|
|
wefaxLiveCtx = canvas.getContext("2d");
|
|
if (!wefaxLiveCtx) return;
|
|
wefaxLiveCtx.putImageData(trimmed, 0, 0);
|
|
try {
|
|
msg._dataUrl = canvas.toDataURL("image/png");
|
|
} catch {
|
|
}
|
|
}
|
|
wefaxImageHistory.unshift(msg);
|
|
if (wefaxImageHistory.length > WEFAX_MAX_IMAGES) {
|
|
wefaxImageHistory = wefaxImageHistory.slice(0, WEFAX_MAX_IMAGES);
|
|
}
|
|
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
|
|
if (wefaxActiveView === "history") {
|
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
|
}
|
|
}
|
|
function onServerWefaxProgress(msg) {
|
|
if (msg.state && !msg.line_data) {
|
|
if (wefaxDom.status) {
|
|
wefaxDom.status.textContent = msg.state;
|
|
wefaxDom.status.style.color = msg.state.indexOf("Idle") === 0 ? "" : "var(--text-accent)";
|
|
}
|
|
return;
|
|
}
|
|
if ((msg.line_count ?? 0) <= 1 || !wefaxLiveCtx) {
|
|
resetLiveCanvas(msg.pixels_per_line || 1809);
|
|
}
|
|
if (msg.line_data) {
|
|
const binary = atob(msg.line_data);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
paintLine(bytes);
|
|
}
|
|
if (wefaxDom.liveInfo) {
|
|
wefaxDom.liveInfo.textContent = `Line ${String(msg.line_count ?? 0)} · ${String(msg.ioc ?? "--")} IOC · ${String(msg.lpm ?? "--")} LPM`;
|
|
}
|
|
if (wefaxDom.status) {
|
|
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
|
|
wefaxDom.status.style.color = "var(--text-accent)";
|
|
}
|
|
}
|
|
function onServerWefax(msg) {
|
|
addWefaxImage(msg);
|
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
|
if (wefaxDom.status) {
|
|
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
|
|
wefaxDom.status.style.color = "";
|
|
}
|
|
}
|
|
function restoreWefaxHistory(messages) {
|
|
if (!messages.length) return;
|
|
for (const message of messages) {
|
|
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
|
|
message._tsMs = tsMs;
|
|
message._ts = new Date(tsMs).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
}
|
|
wefaxImageHistory = messages.concat(wefaxImageHistory);
|
|
pruneWefaxHistory();
|
|
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
|
|
if (wefaxActiveView === "history") {
|
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
|
}
|
|
}
|
|
function pruneWefaxHistoryView() {
|
|
pruneWefaxHistory();
|
|
renderWefaxHistoryTable();
|
|
renderWefaxLatestCard();
|
|
}
|
|
function resetWefaxHistoryView() {
|
|
wefaxImageHistory = [];
|
|
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
|
|
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
|
|
wefaxLiveCtx = null;
|
|
wefaxLiveLineCount = 0;
|
|
renderWefaxLatestCard();
|
|
renderWefaxHistoryTable();
|
|
if (wefaxDom.status) {
|
|
wefaxDom.status.textContent = "Idle";
|
|
wefaxDom.status.style.color = "";
|
|
}
|
|
}
|
|
if (wefaxDom.filterInput) {
|
|
const filterInput = wefaxDom.filterInput;
|
|
wefaxDom.filterInput.addEventListener("input", function() {
|
|
wefaxFilterText = filterInput.value.trim().toUpperCase();
|
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
|
});
|
|
}
|
|
if (wefaxDom.sortSelect) {
|
|
wefaxDom.sortSelect.addEventListener("change", function() {
|
|
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
|
|
});
|
|
}
|
|
wefaxWindow.syncWefaxToggle = function(enabled) {
|
|
if (!wefaxDom.toggleBtn) return;
|
|
wefaxDom.toggleBtn.dataset.enabled = enabled ? "true" : "false";
|
|
wefaxDom.toggleBtn.textContent = enabled ? "Disable WEFAX" : "Enable WEFAX";
|
|
wefaxDom.toggleBtn.style.borderColor = enabled ? "#00d17f" : "";
|
|
wefaxDom.toggleBtn.style.color = enabled ? "#00d17f" : "";
|
|
};
|
|
if (wefaxDom.toggleBtn) {
|
|
const toggleButton = wefaxDom.toggleBtn;
|
|
wefaxDom.toggleBtn.addEventListener("click", () => {
|
|
void (async () => {
|
|
try {
|
|
if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
|
|
await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
|
|
}
|
|
await wefaxWindow.postPath?.("/toggle_wefax_decode");
|
|
} catch (e) {
|
|
console.error("WEFAX toggle failed", e);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
if (wefaxDom.clearBtn) {
|
|
wefaxDom.clearBtn.addEventListener("click", () => {
|
|
void (async () => {
|
|
try {
|
|
await wefaxWindow.postPath?.("/clear_wefax_decode");
|
|
resetWefaxHistoryView();
|
|
} catch (e) {
|
|
console.error("WEFAX clear failed", e);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
renderWefaxLatestCard();
|
|
wefaxWindow.trxPluginRuntime.registerDecoder({
|
|
id: "wefax",
|
|
onMessage: onServerWefax,
|
|
restore: restoreWefaxHistory,
|
|
prune: pruneWefaxHistoryView,
|
|
reset: resetWefaxHistoryView
|
|
});
|
|
wefaxWindow.trxPluginRuntime.registerDecoder({
|
|
id: "wefax_progress",
|
|
onMessage: onServerWefaxProgress
|
|
});
|