Let the operator edit the station config in the window instead of hand-editing the file and relaunching.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+132
-3
@@ -9,11 +9,31 @@ main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
#connection {
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
#connection {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#gear {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #f2f4f6;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
#protocol {
|
||||
min-height: 1.4em;
|
||||
margin: 0;
|
||||
@@ -24,7 +44,7 @@ main {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
#ptt {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
@@ -36,6 +56,115 @@ button {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button.down {
|
||||
#ptt.down {
|
||||
background: #9b2226;
|
||||
}
|
||||
|
||||
#ptt:disabled {
|
||||
background: #3a4248;
|
||||
color: #9aa3ab;
|
||||
}
|
||||
|
||||
#config {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
background: rgba(16, 20, 24, 0.82);
|
||||
}
|
||||
|
||||
#config[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#config form {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid #2c3640;
|
||||
border-radius: 8px;
|
||||
background: #101418;
|
||||
}
|
||||
|
||||
#config h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#config-error {
|
||||
margin: 0 0 12px;
|
||||
color: #f4a261;
|
||||
}
|
||||
|
||||
#config label {
|
||||
display: block;
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#config input {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid #2c3640;
|
||||
border-radius: 6px;
|
||||
background: #0c1014;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
#config .check input {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
margin: 0 0 0 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.secret {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secret input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#auth-toggle,
|
||||
#cancel,
|
||||
#save {
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
#auth-toggle {
|
||||
width: 72px;
|
||||
background: #243038;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#cancel {
|
||||
background: #243038;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#save {
|
||||
background: #2d6a4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
Vendored
+162
-1
@@ -1,4 +1,14 @@
|
||||
const button = document.getElementById("ptt");
|
||||
const config = document.getElementById("config");
|
||||
const configForm = document.getElementById("config-form");
|
||||
const configError = document.getElementById("config-error");
|
||||
const authKey = document.getElementById("authKey");
|
||||
const authToggle = document.getElementById("auth-toggle");
|
||||
|
||||
const configHeight = 680;
|
||||
let savedSize = null;
|
||||
let configLoading = false;
|
||||
let sizeToken = 0;
|
||||
|
||||
function api() {
|
||||
if (!window.go || !window.go.main || !window.go.main.App) {
|
||||
@@ -17,6 +27,104 @@ function line(state) {
|
||||
return "Idle";
|
||||
}
|
||||
|
||||
function runtime() {
|
||||
return window.runtime || null;
|
||||
}
|
||||
|
||||
function growWindow() {
|
||||
const rt = runtime();
|
||||
if (!rt || !rt.WindowGetSize || !rt.WindowSetSize) {
|
||||
return;
|
||||
}
|
||||
const token = ++sizeToken;
|
||||
rt.WindowGetSize().then(function (size) {
|
||||
if (token !== sizeToken || config.hidden) {
|
||||
return;
|
||||
}
|
||||
savedSize = size;
|
||||
rt.WindowSetSize(Math.max(size.w, 420), configHeight);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreWindow() {
|
||||
sizeToken++;
|
||||
const rt = runtime();
|
||||
if (!savedSize || !rt || !rt.WindowSetSize) {
|
||||
savedSize = null;
|
||||
return;
|
||||
}
|
||||
rt.WindowSetSize(savedSize.w, savedSize.h);
|
||||
savedSize = null;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
configError.hidden = !message;
|
||||
configError.textContent = message || "";
|
||||
}
|
||||
|
||||
function fillForm(form) {
|
||||
document.getElementById("host").value = form.host || "";
|
||||
document.getElementById("port").value = String(form.port || 0);
|
||||
document.getElementById("callsign-field").value = form.callsign || "";
|
||||
authKey.value = form.authKey || "";
|
||||
document.getElementById("talkGroup").value = String(form.talkGroup || 0);
|
||||
document.getElementById("certificateDir").value = form.certificateDir || "";
|
||||
document.getElementById("enableLogging").checked = !!form.enableLogging;
|
||||
document.getElementById("logFile").value = form.logFile || "";
|
||||
authKey.type = "password";
|
||||
authToggle.textContent = "Show";
|
||||
}
|
||||
|
||||
function readForm() {
|
||||
return {
|
||||
host: document.getElementById("host").value,
|
||||
port: numberValue("port"),
|
||||
callsign: document.getElementById("callsign-field").value,
|
||||
authKey: authKey.value,
|
||||
talkGroup: numberValue("talkGroup"),
|
||||
certificateDir: document.getElementById("certificateDir").value,
|
||||
enableLogging: document.getElementById("enableLogging").checked,
|
||||
logFile: document.getElementById("logFile").value
|
||||
};
|
||||
}
|
||||
|
||||
function numberValue(id) {
|
||||
const text = document.getElementById(id).value.trim();
|
||||
if (text === "") {
|
||||
return 0;
|
||||
}
|
||||
const n = Number(text);
|
||||
if (!Number.isInteger(n)) {
|
||||
return 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function showConfig() {
|
||||
const app = api();
|
||||
if (!app || configLoading) {
|
||||
return;
|
||||
}
|
||||
configLoading = true;
|
||||
app.Config().then(function (cfg) {
|
||||
if (!cfg.open) {
|
||||
return;
|
||||
}
|
||||
fillForm(cfg.form || {});
|
||||
showError(cfg.error);
|
||||
config.hidden = false;
|
||||
growWindow();
|
||||
}).catch(function () {}).then(function () {
|
||||
configLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function hideConfig() {
|
||||
config.hidden = true;
|
||||
showError("");
|
||||
restoreWindow();
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
document.getElementById("connection").textContent = state.connection;
|
||||
document.getElementById("callsign").textContent = state.callsign;
|
||||
@@ -25,6 +133,12 @@ function render(state) {
|
||||
protocol.textContent = state.protocol ? "Protocol " + state.protocol : "";
|
||||
document.getElementById("detail").textContent = line(state);
|
||||
button.classList.toggle("down", !!state.transmitting);
|
||||
button.disabled = !!state.configOpen;
|
||||
if (state.configOpen && config.hidden) {
|
||||
showConfig();
|
||||
} else if (!state.configOpen && !config.hidden) {
|
||||
hideConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
@@ -35,7 +149,47 @@ function refresh() {
|
||||
app.State().then(render).catch(function () {});
|
||||
}
|
||||
|
||||
function cancelConfig() {
|
||||
const app = api();
|
||||
if (app) {
|
||||
app.CancelConfig();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("gear").addEventListener("click", function () {
|
||||
const app = api();
|
||||
if (app) {
|
||||
app.OpenConfig();
|
||||
}
|
||||
});
|
||||
|
||||
configForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const app = api();
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
app.Save(readForm()).then(function () {
|
||||
refresh();
|
||||
}).catch(function () {
|
||||
app.Config().then(function (cfg) {
|
||||
showError(cfg.error);
|
||||
}).catch(function () {});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("cancel").addEventListener("click", cancelConfig);
|
||||
|
||||
authToggle.addEventListener("click", function () {
|
||||
const show = authKey.type === "password";
|
||||
authKey.type = show ? "text" : "password";
|
||||
authToggle.textContent = show ? "Hide" : "Show";
|
||||
});
|
||||
|
||||
button.addEventListener("pointerdown", function (event) {
|
||||
if (button.disabled) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
button.setPointerCapture(event.pointerId);
|
||||
const app = api();
|
||||
@@ -57,11 +211,18 @@ button.addEventListener("pointercancel", releaseButton);
|
||||
let spaceHeld = false;
|
||||
|
||||
window.addEventListener("keydown", function (event) {
|
||||
if (event.code === "Escape") {
|
||||
if (!config.hidden) {
|
||||
event.preventDefault();
|
||||
cancelConfig();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.code !== "Space") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (event.repeat || spaceHeld) {
|
||||
if (!config.hidden || button.disabled || event.repeat || spaceHeld) {
|
||||
return;
|
||||
}
|
||||
spaceHeld = true;
|
||||
|
||||
Vendored
+27
-1
@@ -7,13 +7,39 @@
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p id="connection">Disconnected</p>
|
||||
<div class="status">
|
||||
<p id="connection">Disconnected</p>
|
||||
<button id="gear" type="button" aria-label="Config">⚙</button>
|
||||
</div>
|
||||
<p>Callsign <span id="callsign"></span></p>
|
||||
<p>Talk group <span id="talkgroup"></span></p>
|
||||
<p id="protocol"></p>
|
||||
<p id="detail">Idle</p>
|
||||
<button id="ptt" type="button">Push to talk</button>
|
||||
</main>
|
||||
<div id="config" hidden>
|
||||
<form id="config-form">
|
||||
<h1>Config</h1>
|
||||
<p id="config-error" hidden></p>
|
||||
<label>Host <input id="host" name="host" autocomplete="off"></label>
|
||||
<label>Port <input id="port" name="port" inputmode="numeric" autocomplete="off"></label>
|
||||
<label>Callsign <input id="callsign-field" name="callsign" autocomplete="off"></label>
|
||||
<label>Auth key
|
||||
<span class="secret">
|
||||
<input id="authKey" name="authKey" type="password" autocomplete="off">
|
||||
<button id="auth-toggle" type="button">Show</button>
|
||||
</span>
|
||||
</label>
|
||||
<label>Talk group <input id="talkGroup" name="talkGroup" inputmode="numeric" autocomplete="off"></label>
|
||||
<label>Certificate directory <input id="certificateDir" name="certificateDir" autocomplete="off"></label>
|
||||
<label class="check">Enable logging <input id="enableLogging" name="enableLogging" type="checkbox"></label>
|
||||
<label>Log file <input id="logFile" name="logFile" autocomplete="off"></label>
|
||||
<div class="actions">
|
||||
<button id="cancel" type="button">Cancel</button>
|
||||
<button id="save" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user