CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m6s
CI / frontend (pull_request) Successful in 5m17s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m26s
557 lines
21 KiB
TypeScript
557 lines
21 KiB
TypeScript
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import { hostState } from "./host.js";
|
|
import { hasAuthRole, type AuthRole } from "../api/auth.js";
|
|
|
|
export {};
|
|
|
|
interface DecoderDescriptor {
|
|
id: string;
|
|
background_decode?: boolean;
|
|
activation?: string;
|
|
active_modes: string[];
|
|
}
|
|
interface Bookmark {
|
|
id: string;
|
|
name: string;
|
|
freq_hz: number;
|
|
mode: string;
|
|
decoders?: string[];
|
|
}
|
|
interface BackgroundDecodeConfig {
|
|
remote: string | null;
|
|
enabled: boolean;
|
|
bookmark_ids: string[];
|
|
}
|
|
interface BackgroundStatusEntry {
|
|
bookmark_name?: string;
|
|
bookmark_id?: string;
|
|
freq_hz?: number;
|
|
mode?: string;
|
|
decoder_kinds?: string[];
|
|
state?: string;
|
|
}
|
|
interface BackgroundDecodeStatus {
|
|
entries?: BackgroundStatusEntry[];
|
|
active_rig?: boolean;
|
|
center_hz?: number;
|
|
sample_rate?: number;
|
|
}
|
|
interface BackgroundBridge {
|
|
decoderRegistry?: DecoderDescriptor[];
|
|
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
|
trx?: { modules?: { backgroundDecode?: BackgroundDecodeService } };
|
|
}
|
|
interface BackgroundDecodeService {
|
|
initialize(rigId: string | null, roles: readonly AuthRole[]): void;
|
|
wireEvents(): void;
|
|
setRig(rigId: string | null): void;
|
|
}
|
|
interface WiredElement extends HTMLElement { _wired?: boolean }
|
|
const bgdWindow = window as unknown as BackgroundBridge;
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
function bgdSupportedIds(): string[] {
|
|
return (bgdWindow.decoderRegistry || [])
|
|
.filter(function (d) { return d.background_decode; })
|
|
.map(function (d) { return d.id; });
|
|
}
|
|
|
|
let backgroundDecodeRoles: readonly AuthRole[] = [];
|
|
let currentRigId: string | null = null;
|
|
let currentConfig: BackgroundDecodeConfig | null = null;
|
|
let bookmarkList: Bookmark[] = [];
|
|
let statusInterval: ReturnType<typeof setInterval> | null = null;
|
|
let bgdDirty = false;
|
|
/** Last polled state per bookmark, so a row can say what it is doing. */
|
|
let statusByBookmark = new Map<string, BackgroundStatusEntry>();
|
|
let lastStatus: BackgroundDecodeStatus | null = null;
|
|
|
|
function initBackgroundDecode(rigId: string | null, roles: readonly AuthRole[]): void {
|
|
backgroundDecodeRoles = roles;
|
|
// The panel used to take whatever rig it was handed at load and wait to be
|
|
// told again. Loading before the rig list arrives handed it null, and the
|
|
// next telling only came when the operator switched rigs, so the panel sat
|
|
// empty and silent. The host knows the rig; ask it.
|
|
currentRigId = rigId || hostState.lastActiveRigId || null;
|
|
if (currentRigId) loadBackgroundDecode();
|
|
startStatusPolling();
|
|
}
|
|
|
|
function setBackgroundDecodeRig(rigId: string | null): void {
|
|
const nextRigId = rigId || null;
|
|
if (nextRigId === currentRigId) return;
|
|
currentRigId = nextRigId;
|
|
if (!currentRigId) return;
|
|
loadBackgroundDecode();
|
|
}
|
|
|
|
function apiGetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
|
|
return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function (r) {
|
|
if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
|
|
return r.json() as Promise<BackgroundDecodeConfig>;
|
|
});
|
|
}
|
|
|
|
function apiPutConfig(rigId: string, config: BackgroundDecodeConfig): Promise<BackgroundDecodeConfig> {
|
|
return fetch("/background-decode/" + encodeURIComponent(rigId), {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(config),
|
|
}).then(function (r) {
|
|
if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
|
|
return r.json() as Promise<BackgroundDecodeConfig>;
|
|
});
|
|
}
|
|
|
|
function apiResetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
|
|
return fetch("/background-decode/" + encodeURIComponent(rigId), {
|
|
method: "DELETE",
|
|
}).then(function (r) {
|
|
if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
|
|
return r.json() as Promise<BackgroundDecodeConfig>;
|
|
});
|
|
}
|
|
|
|
function apiGetStatus(rigId: string): Promise<BackgroundDecodeStatus> {
|
|
return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function (r) {
|
|
if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
|
|
return r.json() as Promise<BackgroundDecodeStatus>;
|
|
});
|
|
}
|
|
|
|
function apiGetBookmarks(): Promise<Bookmark[]> {
|
|
return fetch("/bookmarks").then(function (r) {
|
|
if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
|
|
return r.json() as Promise<Bookmark[]>;
|
|
});
|
|
}
|
|
|
|
function loadBackgroundDecode() {
|
|
const rigId = currentRigId;
|
|
if (!rigId) return;
|
|
Promise.all([apiGetConfig(rigId), apiGetBookmarks()])
|
|
.then(function ([config, bookmarks]) {
|
|
currentConfig = config;
|
|
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
|
|
renderBackgroundDecode();
|
|
clearBgdDirty();
|
|
pollBackgroundDecodeStatus();
|
|
})
|
|
.catch(function (err: unknown) {
|
|
console.error("background decode load failed", err);
|
|
});
|
|
}
|
|
|
|
function supportedBookmarks(): Bookmark[] {
|
|
return bookmarkList.filter(function (bookmark) {
|
|
return bookmarkDecoderKinds(bookmark).length > 0;
|
|
});
|
|
}
|
|
|
|
function bookmarkDecoderKinds(bookmark: Bookmark): string[] {
|
|
const ids = bgdSupportedIds();
|
|
const decoders = bookmark.decoders ?? [];
|
|
const explicit = decoders
|
|
.map(function (item) { return item.trim().toLowerCase(); })
|
|
.filter(function (item, index, arr) {
|
|
return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
|
|
});
|
|
if (explicit.length > 0) return explicit;
|
|
// Fall back: infer from mode via mode-bound entries in the registry.
|
|
const mode = bookmark.mode.trim().toUpperCase();
|
|
return (bgdWindow.decoderRegistry || [])
|
|
.filter(function (d) {
|
|
return d.activation === "mode_bound" && d.background_decode
|
|
&& d.active_modes.indexOf(mode) >= 0;
|
|
})
|
|
.map(function (d) { return d.id; });
|
|
}
|
|
|
|
function renderBackgroundDecode() {
|
|
if (!currentConfig) {
|
|
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
|
|
}
|
|
setCheckbox("background-decode-enabled", currentConfig.enabled);
|
|
renderBookmarkChecklist();
|
|
|
|
const isControl = isControlRole();
|
|
const panel = document.getElementById("background-decode-panel");
|
|
if (panel) {
|
|
panel.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>("input, select, button.sch-write").forEach(function (el) {
|
|
el.disabled = !isControl;
|
|
});
|
|
}
|
|
const saveBtn = document.getElementById("background-decode-save-btn");
|
|
const resetBtn = document.getElementById("background-decode-reset-btn");
|
|
if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
|
|
if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
|
|
syncSaveButton();
|
|
}
|
|
|
|
function currentFilterText(): string {
|
|
return (document.getElementById("bgd-bookmark-filter") as HTMLInputElement | null)?.value ?? "";
|
|
}
|
|
|
|
function renderBookmarkChecklist(filterText = ""): void {
|
|
const container = document.getElementById("bgd-bookmark-checklist");
|
|
if (!container) return;
|
|
container.innerHTML = "";
|
|
|
|
const selectedIds = new Set(
|
|
currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
|
|
);
|
|
const all = supportedBookmarks();
|
|
const filter = (filterText || "").trim().toLowerCase();
|
|
|
|
const filtered = filter
|
|
? all.filter(function (bm) {
|
|
const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
|
|
return text.indexOf(filter) >= 0;
|
|
})
|
|
: all;
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div class="bgd-checklist-empty">' + escHtml(emptyListText(all.length)) + "</div>";
|
|
renderSelectionSummary();
|
|
return;
|
|
}
|
|
|
|
filtered.forEach(function (bookmark) {
|
|
const row = document.createElement("label");
|
|
row.className = "bgd-checklist-row";
|
|
row.dataset.bmId = bookmark.id;
|
|
const decoders = bookmarkDecoderKinds(bookmark);
|
|
const selected = selectedIds.has(bookmark.id);
|
|
if (selected) row.classList.add("is-selected");
|
|
row.innerHTML =
|
|
'<input type="checkbox"' + (selected ? " checked" : "") + ' data-bm-id="' + escHtml(bookmark.id) + '" />' +
|
|
'<span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span>' +
|
|
'<span class="bgd-checklist-meta">'
|
|
+ escHtml(formatFreq(bookmark.freq_hz)) + '<span class="bgd-checklist-mode">'
|
|
+ escHtml(bookmark.mode + " · " + decoders.join("/").toUpperCase()) + '</span></span>'
|
|
+ stateBadgeHtml(bookmark.id, selected);
|
|
row.querySelector<HTMLInputElement>("input")?.addEventListener("change", function (e) {
|
|
onChecklistToggle(bookmark.id, (e.currentTarget as HTMLInputElement).checked);
|
|
});
|
|
container.appendChild(row);
|
|
});
|
|
renderSelectionSummary();
|
|
}
|
|
|
|
/** Why the list is empty, in the terms the operator can act on. */
|
|
function emptyListText(supportedCount: number): string {
|
|
if (supportedCount > 0) return "No bookmark matches that filter.";
|
|
if (bookmarkList.length > 0) {
|
|
return "None of your bookmarks name a decoder that can run in the background."
|
|
+ " Give one a decoder on the Bookmarks tab to list it here.";
|
|
}
|
|
return "No bookmarks yet. Save one on the Bookmarks tab and it can be decoded here.";
|
|
}
|
|
|
|
/** The live state of a selected bookmark, as the row's own badge. */
|
|
function stateBadgeHtml(bookmarkId: string, selected: boolean): string {
|
|
if (!selected) return '<span class="bgd-state" data-state="unselected"></span>';
|
|
const entry = statusByBookmark.get(bookmarkId);
|
|
const state = entry?.state ?? (currentConfig?.enabled ? "pending" : "disabled");
|
|
return '<span class="bgd-state" data-state="' + escHtml(state) + '" title="' + escHtml(stateHelp(state)) + '">'
|
|
+ '<span class="bgd-state-dot"></span>' + escHtml(prettyState(state)) + "</span>";
|
|
}
|
|
|
|
function renderSelectionSummary(): void {
|
|
const el = document.getElementById("bgd-selection-summary");
|
|
if (!el) return;
|
|
const selected = currentConfig?.bookmark_ids.length ?? 0;
|
|
if (selected === 0) {
|
|
el.textContent = "Nothing selected — background decoding is idle.";
|
|
return;
|
|
}
|
|
const active = [...statusByBookmark.values()].filter((entry) => entry.state === "active").length;
|
|
const noun = `${String(selected)} bookmark${selected === 1 ? "" : "s"} selected`;
|
|
el.textContent = currentConfig?.enabled
|
|
? `${noun}, ${String(active)} decoding now.`
|
|
: `${noun}. Switch Enabled on to start decoding them.`;
|
|
}
|
|
|
|
function onChecklistToggle(bookmarkId: string, checked: boolean): void {
|
|
if (!currentConfig) {
|
|
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
|
|
}
|
|
if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
|
|
if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
|
|
currentConfig.bookmark_ids.push(bookmarkId);
|
|
} else if (!checked) {
|
|
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (id) { return id !== bookmarkId; });
|
|
}
|
|
markBgdDirty();
|
|
}
|
|
|
|
function saveBackgroundDecode() {
|
|
const rigId = currentRigId;
|
|
if (!rigId) return;
|
|
const payload: BackgroundDecodeConfig = {
|
|
remote: rigId,
|
|
enabled: (document.getElementById("background-decode-enabled") as HTMLInputElement | null)?.checked ?? false,
|
|
bookmark_ids: currentConfig?.bookmark_ids.slice() ?? [],
|
|
};
|
|
const btn = document.getElementById("background-decode-save-btn") as HTMLButtonElement | null;
|
|
if (btn) btn.disabled = true;
|
|
apiPutConfig(rigId, payload)
|
|
.then(function (saved) {
|
|
currentConfig = saved;
|
|
renderBackgroundDecode();
|
|
clearBgdDirty();
|
|
pollBackgroundDecodeStatus();
|
|
showToast("Background decode saved.", false);
|
|
})
|
|
.catch(function (err: unknown) {
|
|
showToast(`Save failed: ${errorMessage(err)}`, true);
|
|
})
|
|
.finally(function () {
|
|
syncSaveButton();
|
|
});
|
|
}
|
|
|
|
async function resetBackgroundDecode() {
|
|
const rigId = currentRigId;
|
|
if (!rigId) return;
|
|
if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
|
|
apiResetConfig(rigId)
|
|
.then(function (saved) {
|
|
currentConfig = saved;
|
|
renderBackgroundDecode();
|
|
clearBgdDirty();
|
|
pollBackgroundDecodeStatus();
|
|
showToast("Background decode reset.", false);
|
|
})
|
|
.catch(function (err: unknown) {
|
|
showToast(`Reset failed: ${errorMessage(err)}`, true);
|
|
});
|
|
}
|
|
|
|
function startStatusPolling() {
|
|
if (statusInterval) clearInterval(statusInterval);
|
|
statusInterval = setInterval(pollBackgroundDecodeStatus, 15000);
|
|
}
|
|
|
|
function pollBackgroundDecodeStatus() {
|
|
const rigId = currentRigId;
|
|
if (!rigId) return;
|
|
apiGetStatus(rigId)
|
|
.then(renderStatus)
|
|
.catch(function () {});
|
|
}
|
|
|
|
function renderStatus(status: BackgroundDecodeStatus): void {
|
|
lastStatus = status;
|
|
statusByBookmark = new Map(
|
|
(status.entries ?? [])
|
|
.filter((entry) => typeof entry.bookmark_id === "string" && entry.bookmark_id.length > 0)
|
|
.map((entry) => [entry.bookmark_id as string, entry]),
|
|
);
|
|
renderSpanSummary();
|
|
// Rows carry the state, so the list repaints — keeping the filter the
|
|
// operator typed and the selection they have not saved yet.
|
|
renderBookmarkChecklist(currentFilterText());
|
|
}
|
|
|
|
/** What the rig is listening across, which is what decides whether a
|
|
* selected bookmark can be decoded at all. */
|
|
function renderSpanSummary(): void {
|
|
const el = document.getElementById("bgd-span-summary");
|
|
if (!el) return;
|
|
const status = lastStatus;
|
|
if (!status) {
|
|
el.textContent = "";
|
|
return;
|
|
}
|
|
if (!status.active_rig) {
|
|
el.textContent = "This rig is not the one playing audio.";
|
|
el.dataset.tone = "warn";
|
|
return;
|
|
}
|
|
const centre = typeof status.center_hz === "number" && Number.isFinite(status.center_hz)
|
|
? formatFreq(status.center_hz)
|
|
: null;
|
|
const half = typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0
|
|
? formatFreq(status.sample_rate / 2)
|
|
: null;
|
|
el.dataset.tone = "";
|
|
el.textContent = centre && half ? `Span ${centre} ±${half}` : centre ? `Centre ${centre}` : "";
|
|
}
|
|
|
|
/** The one-line reason behind a state, for the row's tooltip. */
|
|
function stateHelp(state: string | undefined): string {
|
|
switch (state) {
|
|
case "active": return "Decoding on a hidden channel.";
|
|
case "out_of_span": return "Outside the span the rig is tuned across, so it cannot be heard from here.";
|
|
case "waiting_for_spectrum": return "Waiting for the first spectrum frame from the rig.";
|
|
case "waiting_for_user": return "Nobody is listening to this rig, so no audio is being pulled.";
|
|
case "missing_bookmark": return "The bookmark this was selected from is gone.";
|
|
case "no_supported_decoders": return "No decoder that runs in the background can decode this bookmark.";
|
|
case "disabled": return "Background decoding is switched off.";
|
|
case "handled_by_scheduler":
|
|
case "scheduler_has_control": return "The scheduler is running this bookmark instead.";
|
|
case "handled_by_virtual_channel": return "A virtual channel is already on this frequency.";
|
|
case "pending": return "Selected, and not started yet — save to apply.";
|
|
default: return "Selected, but not decoding.";
|
|
}
|
|
}
|
|
|
|
// The dot beside the word already says whether this is running, so the
|
|
// words no longer carry a tick or a triangle of their own.
|
|
function prettyState(state: string | undefined): string {
|
|
switch (state) {
|
|
case "active": return "Decoding";
|
|
case "out_of_span": return "Out of span";
|
|
case "waiting_for_spectrum": return "Waiting for spectrum";
|
|
case "waiting_for_user": return "Nobody listening";
|
|
case "missing_bookmark": return "Bookmark gone";
|
|
case "no_supported_decoders": return "No decoder";
|
|
case "disabled": return "Off";
|
|
case "handled_by_scheduler":
|
|
case "scheduler_has_control": return "Scheduler has it";
|
|
case "handled_by_virtual_channel": return "On a channel";
|
|
case "pending": return "Not saved";
|
|
default: return "Idle";
|
|
}
|
|
}
|
|
|
|
function setCheckbox(id: string, value: boolean): void {
|
|
const el = document.getElementById(id) as HTMLInputElement | null;
|
|
if (el) el.checked = value;
|
|
}
|
|
|
|
function formatFreq(hz: number): string {
|
|
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
|
if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
|
|
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
|
|
return `${String(hz)} Hz`;
|
|
}
|
|
|
|
function escHtml(value: unknown): string {
|
|
const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
? String(value)
|
|
: "";
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
function markBgdDirty() {
|
|
if (bgdDirty) return;
|
|
bgdDirty = true;
|
|
syncSaveButton();
|
|
}
|
|
|
|
function clearBgdDirty() {
|
|
bgdDirty = false;
|
|
syncSaveButton();
|
|
}
|
|
|
|
/** Save offers itself only when there is something to save. */
|
|
function syncSaveButton() {
|
|
const btn = document.getElementById("background-decode-save-btn") as HTMLButtonElement | null;
|
|
if (!btn) return;
|
|
btn.classList.toggle("sch-dirty", bgdDirty);
|
|
btn.disabled = !bgdDirty || !isControlRole();
|
|
btn.title = bgdDirty ? "Apply these bookmarks to the background decoder" : "No changes to save";
|
|
}
|
|
|
|
function isControlRole(): boolean {
|
|
return hasAuthRole(backgroundDecodeRoles, "control") || hostState.authEnabled === false;
|
|
}
|
|
|
|
function showToast(msg: string, isError: boolean): void {
|
|
const el = document.getElementById("background-decode-toast");
|
|
if (!el) return;
|
|
el.textContent = msg;
|
|
el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
|
|
el.style.display = "block";
|
|
setTimeout(function () {
|
|
el.style.display = "none";
|
|
}, 3000);
|
|
}
|
|
|
|
function selectAllBookmarks() {
|
|
if (!currentConfig) {
|
|
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
|
|
}
|
|
const ids = supportedBookmarks().map(function (bm) { return bm.id; });
|
|
currentConfig.bookmark_ids = ids;
|
|
renderBookmarkChecklist(currentFilterText());
|
|
markBgdDirty();
|
|
}
|
|
|
|
function deselectAllBookmarks() {
|
|
if (!currentConfig) {
|
|
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
|
|
}
|
|
currentConfig.bookmark_ids = [];
|
|
renderBookmarkChecklist(currentFilterText());
|
|
markBgdDirty();
|
|
}
|
|
|
|
function wireBackgroundDecodeEvents() {
|
|
const filterInput = document.getElementById("bgd-bookmark-filter") as (HTMLInputElement & WiredElement) | null;
|
|
if (filterInput && !filterInput._wired) {
|
|
filterInput._wired = true;
|
|
filterInput.addEventListener("input", function () {
|
|
renderBookmarkChecklist(filterInput.value);
|
|
});
|
|
}
|
|
|
|
const enabledCb = document.getElementById("background-decode-enabled") as (HTMLInputElement & WiredElement) | null;
|
|
if (enabledCb && !enabledCb._wired) {
|
|
enabledCb._wired = true;
|
|
enabledCb.addEventListener("change", function () {
|
|
if (currentConfig) currentConfig.enabled = enabledCb.checked;
|
|
markBgdDirty();
|
|
renderBookmarkChecklist(currentFilterText());
|
|
});
|
|
}
|
|
|
|
const selectAllBtn = document.getElementById("bgd-select-all-btn") as WiredElement | null;
|
|
if (selectAllBtn && !selectAllBtn._wired) {
|
|
selectAllBtn._wired = true;
|
|
selectAllBtn.addEventListener("click", selectAllBookmarks);
|
|
}
|
|
|
|
const deselectAllBtn = document.getElementById("bgd-deselect-all-btn") as WiredElement | null;
|
|
if (deselectAllBtn && !deselectAllBtn._wired) {
|
|
deselectAllBtn._wired = true;
|
|
deselectAllBtn.addEventListener("click", deselectAllBookmarks);
|
|
}
|
|
|
|
const saveBtn = document.getElementById("background-decode-save-btn") as WiredElement | null;
|
|
if (saveBtn && !saveBtn._wired) {
|
|
saveBtn._wired = true;
|
|
saveBtn.addEventListener("click", saveBackgroundDecode);
|
|
}
|
|
|
|
const resetBtn = document.getElementById("background-decode-reset-btn") as WiredElement | null;
|
|
if (resetBtn && !resetBtn._wired) {
|
|
resetBtn._wired = true;
|
|
resetBtn.addEventListener("click", () => { void resetBackgroundDecode(); });
|
|
}
|
|
}
|
|
|
|
bgdWindow.trx ??= {};
|
|
bgdWindow.trx.modules ??= {};
|
|
bgdWindow.trx.modules.backgroundDecode = {
|
|
initialize: initBackgroundDecode,
|
|
wireEvents: wireBackgroundDecodeEvents,
|
|
setRig: setBackgroundDecodeRig,
|
|
};
|
|
})();
|