add notifications and better url handling

This commit is contained in:
Dennis Gunia
2025-10-22 23:11:02 +02:00
parent 1ae8180df4
commit b24b5f674e
3 changed files with 192 additions and 66 deletions

View File

@@ -0,0 +1,53 @@
.notification-container {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 9;
width: 100%;
padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);
}
.notification {
width: 100% - var(--pico-block-spacing-horizontal);
height: fit-content;
margin: var(--pico-spacing);
overflow: auto;
padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);
border-radius: var(--pico-border-radius);
box-shadow: var(--pico-card-box-shadow);
transition: opacity 1s ease;
opacity: 0.0;
backdrop-filter: blur(10px);
}
.notification>p {
font-weight: bold;
padding-bottom: 0%;
margin-bottom: 0;
}
.notification-success {
background: rgb(1, 82, 52);
}
.notification-success>p {
color: rgb(33, 226, 153);
}
.notification-warn {
background: rgb(139, 79, 0);
}
.notification-warn>p {
color: rgb(254, 182, 112);
}
.notification-error {
background: rgb(175, 41, 29);
}
.notification-error>p {
color: rgb(245, 163, 144)
}

View File

@@ -15,31 +15,12 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="css/pico.min.css"> <link rel="stylesheet" href="css/pico.min.css">
<link rel="stylesheet" href="css/sfc.css">
<script type="text/javascript" src="js/sfc.js"></script> <script type="text/javascript" src="js/sfc.js"></script>
<title>Hello world!</title> <title>Hello world!</title>
</head> </head>
<body> <body>
<dialog id="dialog_change_calibration">
<article>
<h2>Change calibration</h2>
<section>
<form id="form_change_calibration">
<label>
New calibration data (default 1400):
<input id="form_change_calibration_data" name="calibration" type="number"
placeholder="Calibration Value" value="1400" />
</label>
</form>
<footer>
<button class="secondary btn_cancel">
Cancel
</button>
<button class="btn_confirm">Confirm</button>
</footer>
</article>
</dialog>
<dialog id="change_address"> <dialog id="change_address">
<article> <article>
<h2>Change address</h2> <h2>Change address</h2>
@@ -88,6 +69,14 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
</dialog> </dialog>
<main class="container"> <main class="container">
<div id="message_container" style="width: 100%;" class="notification-container">
<template id="notification">
<div class="notification">
<p>Notification</p>
</div>
</template>
</div>
<nav> <nav>
<ul> <ul>
<li><strong>SplitFlap</strong></li> <li><strong>SplitFlap</strong></li>
@@ -95,7 +84,7 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
<ul> <ul>
<li><a onclick="changeView('view_display')" class="secondary">Display</a></li> <li><a onclick="changeView('view_display')" class="secondary">Display</a></li>
<li> <li>
<details class="dropdown"> <details class="dropdown" id="menu_dropdown">
<summary> <summary>
Configuration Configuration
</summary> </summary>
@@ -108,6 +97,7 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
</li> </li>
</ul> </ul>
</nav> </nav>
<div id="view_display"><!-- Main View : print --> <div id="view_display"><!-- Main View : print -->
<h1>Display Something!</h1> <h1>Display Something!</h1>
<form id="form_display"> <form id="form_display">
@@ -123,6 +113,16 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
<input id="form_display_y" name="y" type="y" placeholder="Position Y" value="0" /> <input id="form_display_y" name="y" type="y" placeholder="Position Y" value="0" />
</fieldset> </fieldset>
</label> </label>
<label>
Display Mode:
<select id="form_display_mode" name="mode" required>
<option selected disabled value="">
Select display mode
</option>
<option value="true">Full Rotation</option>
<option value="false">Direct</option>
</select>
</label>
</fieldset> </fieldset>
<input type="button" onClick="btn_display()" value="Display" /> <input type="button" onClick="btn_display()" value="Display" />
@@ -218,10 +218,10 @@ of the license in the project repository or at <https://www.gnu.org/licenses/agp
</div> </div>
<div id="view_storage" style="display: none;"><!-- Main View : print --> <div id="view_storage" style="display: none;"><!-- Main View : print -->
<h1>Load/Save Config!</h1> <h1>Load/Save Config!</h1>
<div role="group"> <div role="group">
<button class="btn_load" onclick="btn_load()">Load Config</button> <button class="btn_load" onclick="btn_load()">Load Config</button>
</div> </div>
<div role="group"> <div role="group">
<button class="btn_save" onclick="btn_save()">Safe Config</button> <button class="btn_save" onclick="btn_save()">Safe Config</button>
</div> </div>
</div> </div>

View File

@@ -1,46 +1,110 @@
// Create WebSocket connection. let socket;
let hostname = location.host;
if (hostname === "") {
hostname = "localhost";
}
console.log(`Connecting to ws://${hostname}`);
const socket = new WebSocket(`ws://${hostname}/manage/`); /* After Document load, change view according to url and connect to ws server */
document.addEventListener("DOMContentLoaded", function () {
// Connection opened const urlSection = location.href.split("#")[1];
socket.addEventListener("open", (event) => { if (urlSection) {
changeView(urlSection);
});
socket.addEventListener("message", (event) => {
switch (last_command) {
case "dm_dump":
parse_module_conf(JSON.parse(event.data));
break;
} }
SFCConnect();
}); });
/* function to conenct to ws server */
function SFCConnect() {
// Create WebSocket connection.
let hostname = location.host;
if (hostname === "") {
hostname = "localhost";
}
console.log(`Connecting to ws://${hostname}`);
socket = new WebSocket(`ws://${hostname}/manage/`);
// Connection opened
socket.addEventListener("open", (event) => {
notify("success", "connected!");
load_module_conf();
});
socket.addEventListener("close", (event) => {
notify("warn", "ws connection failed!. Reconnect in 5s");
setTimeout(() => {
SFCConnect();
}, 5000)
});
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data)
console.log(data)
switch (last_command) {
case "dm_dump":
parse_module_conf(data);
break;
}
if (data.ack) {
notify("success", "command sent!");
}
});
}
/* function to send command */
function SFCCommandAsync(data) {
if (socket && socket.readyState !== WebSocket.CLOSED) {
last_command = data.command;
socket.send(JSON.stringify(data));
} else {
notify("error", "ws not connected!");
}
}
/* utility function to format int to hex string */
function toAddressStr(address) { function toAddressStr(address) {
const hexbase = address.toString(16).padStart(4, '0').toUpperCase(); const hexbase = address.toString(16).padStart(4, '0').toUpperCase();
return `0x${hexbase}`; return `0x${hexbase}`;
} }
// Send command let messageCounter = 0;
function SFCCommandAsync(data) { function notify(severity, text) {
last_command = data.command; const template = document.querySelector("#notification");
socket.send(JSON.stringify(data)); const clone = template.content.cloneNode(true);
const tbody = document.querySelector("#message_container");
clone.querySelector("div").classList.add(`notification-${severity}`)
clone.querySelector("div").querySelector("p").innerHTML = text;
const messageDivID = `notification-dyn-${messageCounter}`
clone.querySelector("div").id = messageDivID;
messageCounter += 1;
setTimeout(function () {
//document.querySelector(`#${messageDivID}`).style.transition = "opacity 1s ease";
document.querySelector(`#${messageDivID}`).style.opacity = 0;
setTimeout(function () {
document.querySelector(`#${messageDivID}`).remove();
}, 1001);
}, 2000);
tbody.appendChild(clone);
document.querySelector(`#${messageDivID}`).style.opacity = 0.7;
} }
function btn_display() { function btn_display() {
const text = document.getElementById("form_display_str").value; const text = document.getElementById("form_display_str").value;
const x = Number(document.getElementById("form_display_x").value); const x = Number(document.getElementById("form_display_x").value);
const y = Number(document.getElementById("form_display_y").value); const y = Number(document.getElementById("form_display_y").value);
const mode = document.getElementById("form_display_mode").value;
console.log(mode)
const msg = { const msg = {
"command": "dm_print", "command": "dm_print",
"string": text, "string": text,
"x": x, "x": x,
"y": y "y": y,
"full": mode == "false" ? false : true
} }
SFCCommandAsync(msg); SFCCommandAsync(msg);
} }
@@ -60,6 +124,20 @@ function btn_reset_module(address) {
SFCCommandAsync(msg); SFCCommandAsync(msg);
} }
function btn_save() {
const msg = {
"command": "dm_save",
}
SFCCommandAsync(msg);
}
function btn_load() {
const msg = {
"command": "dm_load",
}
SFCCommandAsync(msg);
}
function changeView(id) { function changeView(id) {
document.getElementById('view_display').style.display = 'none'; document.getElementById('view_display').style.display = 'none';
document.getElementById('view_conf_modules').style.display = 'none'; document.getElementById('view_conf_modules').style.display = 'none';
@@ -72,16 +150,24 @@ function changeView(id) {
load_module_conf(); load_module_conf();
break; break;
} }
// append to url
//location.href = "test"
const newHref = `${location.href.split("#")[0]}#${id}`;
location.href = newHref;
// close menu
document.getElementById('menu_dropdown').removeAttribute('open');
} }
let modules = []; let modules = [];
function load_module_conf() { function load_module_conf() {
document.getElementById('btn_refresh').ariaBusy = 'true'; if (socket) {
document.getElementById('btn_refresh').disabled = true; document.getElementById('btn_refresh').ariaBusy = 'true';
document.getElementById('btn_refresh').disabled = true;
SFCCommandAsync({ "command": "dm_dump" }); SFCCommandAsync({ "command": "dm_dump" });
}
} }
function parse_module_conf(data) { function parse_module_conf(data) {
@@ -97,7 +183,7 @@ function parse_module_conf(data) {
const clone = template.content.cloneNode(true); const clone = template.content.cloneNode(true);
let summary = clone.querySelector("summary"); let summary = clone.querySelector("summary");
summary.textContent = `Module ID ${mod["id"]} : ${mod["status"]["device"]}`; summary.textContent = `Module ${mod["id"]} : ${mod["status"]["device"]} | Addr: ${toAddressStr(mod["address"])} , Pos: (${mod["position"]["x"]}, ${mod["position"]["y"]})`;
switch (mod["status"]["device"]) { switch (mod["status"]["device"]) {
case 'ONLINE': case 'ONLINE':
summary.classList.add("pico-color-jade-500"); summary.classList.add("pico-color-jade-500");
@@ -238,16 +324,3 @@ function display_dialog_add_device() {
} }
} }
function btn_save(){
const msg = {
"command": "dm_save",
}
SFCCommandAsync(msg);
}
function btn_load(){
const msg = {
"command": "dm_load",
}
SFCCommandAsync(msg);
}