1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-27 14:33:52 +01:00

offline optimization, new signal chart, ui optimization

This commit is contained in:
Marek Farkaš
2025-02-23 15:27:46 +01:00
parent fcfc0691fa
commit 3c91c8d06c
24 changed files with 373 additions and 287 deletions

View File

@@ -84,16 +84,20 @@ function handleConnect(clientIp, currentUsers, ws, callback) {
} }
}); });
}).on("error", (err) => { }).on("error", (err) => {
console.error("Error fetching location data:", err); consoleCmd.logError("Error fetching location data:", err.code);
callback("User allowed"); callback("User allowed");
}); });
} }
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { let bannedASCache = { data: null, timestamp: 0 };
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
const connectionTime = new Date().toLocaleString([], options);
https.get("https://fmdx.org/banned_as.json", (banResponse) => { function fetchBannedAS(callback) {
const now = Date.now();
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) {
return callback(null, bannedASCache.data);
}
const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => {
let banData = ""; let banData = "";
banResponse.on("data", (chunk) => { banResponse.on("data", (chunk) => {
@@ -103,39 +107,60 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
banResponse.on("end", () => { banResponse.on("end", () => {
try { try {
const bannedAS = JSON.parse(banData).banned_as || []; const bannedAS = JSON.parse(banData).banned_as || [];
bannedASCache = { data: bannedAS, timestamp: now };
if (bannedAS.some((as) => locationInfo.as?.includes(as))) { callback(null, bannedAS);
return callback("User banned");
}
const userLocation =
locationInfo.country === undefined
? "Unknown"
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
storage.connectedUsers.push({
ip: clientIp,
location: userLocation,
time: connectionTime,
instance: ws,
});
consoleCmd.logInfo(
`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
);
callback("User allowed");
} catch (error) { } catch (error) {
console.error("Error parsing banned AS list:", error); console.error("Error parsing banned AS list:", error);
callback("User allowed"); callback(null, []); // Default to allowing user
} }
}); });
}).on("error", (err) => { });
// Set timeout for the request (5 seconds)
req.setTimeout(5000, () => {
console.error("Error: Request timed out while fetching banned AS list.");
req.abort();
callback(null, []); // Default to allowing user
});
req.on("error", (err) => {
console.error("Error fetching banned AS list:", err); console.error("Error fetching banned AS list:", err);
callback("User allowed"); callback(null, []); // Default to allowing user
}); });
} }
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
const connectionTime = new Date().toLocaleString([], options);
fetchBannedAS((error, bannedAS) => {
if (error) {
console.error("Error fetching banned AS list:", error);
}
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
return callback("User banned");
}
const userLocation =
locationInfo.country === undefined
? "Unknown"
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
storage.connectedUsers.push({
ip: clientIp,
location: userLocation,
time: connectionTime,
instance: ws,
});
consoleCmd.logInfo(
`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
);
callback("User allowed");
});
}
function formatUptime(uptimeInSeconds) { function formatUptime(uptimeInSeconds) {
const secondsInMinute = 60; const secondsInMinute = 60;

View File

@@ -469,11 +469,9 @@ wss.on('connection', (ws, request) => {
}); });
ws.on('close', (code, reason) => { ws.on('close', (code, reason) => {
if (clientIp !== '::ffff:127.0.0.1' || if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
(request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') || currentUsers--;
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { }
currentUsers--;
}
dataHandler.showOnlineUsers(currentUsers); dataHandler.showOnlineUsers(currentUsers);
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);

View File

@@ -13,8 +13,14 @@ let usStatesGeoJson = null; // To cache the GeoJSON data for US states
// Load the US states GeoJSON data // Load the US states GeoJSON data
async function loadUsStatesGeoJson() { async function loadUsStatesGeoJson() {
if (!usStatesGeoJson) { if (!usStatesGeoJson) {
const response = await fetch(usStatesGeoJsonUrl); try {
usStatesGeoJson = await response.json(); const response = await fetch(usStatesGeoJsonUrl);
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
usStatesGeoJson = await response.json();
} catch (error) {
console.error("Failed to load US States GeoJSON:", error);
usStatesGeoJson = null; // Ensure it's null so it can retry later
}
} }
} }
@@ -79,18 +85,17 @@ async function fetchTx(freq, piCode, rdsPs) {
const url = "https://maps.fmdx.org/api/?freq=" + freq; const url = "https://maps.fmdx.org/api/?freq=" + freq;
return fetch(url, { try {
redirect: 'manual' const response = await fetch(url, { redirect: 'manual' });
}) if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
.then(response => response.json()) const data = await response.json();
.then(async (data) => { cachedData[freq] = data;
cachedData[freq] = data; await loadUsStatesGeoJson();
await loadUsStatesGeoJson(); return processData(data, piCode, rdsPs);
return processData(data, piCode, rdsPs); } catch (error) {
}) console.error("Error fetching data:", error);
.catch(error => { return null; // Return null to indicate failure
console.error("Error fetching data:", error); }
});
} }
async function processData(data, piCode, rdsPs) { async function processData(data, piCode, rdsPs) {
@@ -182,29 +187,28 @@ function checkEs() {
let esSwitch = false; let esSwitch = false;
if (now - esSwitchCache.lastCheck < esFetchInterval) { if (now - esSwitchCache.lastCheck < esFetchInterval) {
esSwitch = esSwitchCache.esSwitch; return esSwitchCache.esSwitch;
} else if (serverConfig.identification.lat > 20) {
esSwitchCache.lastCheck = now;
fetch(url)
.then(response => response.json())
.then(data => {
if (serverConfig.identification.lon < -32) {
if (data.north_america.max_frequency != "No data") {
esSwitch = true;
}
} else {
if (data.europe.max_frequency != "No data") {
esSwitch = true;
}
}
esSwitchCache.esSwitch = esSwitch;
})
.catch(error => {
console.error("Error fetching data:", error);
});
} }
return esSwitch; if (serverConfig.identification.lat > 20) {
esSwitchCache.lastCheck = now;
fetch(url)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.json();
})
.then(data => {
if ((serverConfig.identification.lon < -32 && data.north_america.max_frequency !== "No data") ||
(serverConfig.identification.lon >= -32 && data.europe.max_frequency !== "No data")) {
esSwitchCache.esSwitch = true;
}
})
.catch(error => {
console.error("Error fetching Es data:", error);
});
}
return esSwitchCache.esSwitch;
} }
function haversine(lat1, lon1, lat2, lon2) { function haversine(lat1, lon1, lat2, lon2) {

View File

@@ -22,6 +22,15 @@ h1#tuner-name {
transition: 0.3s ease color; transition: 0.3s ease color;
} }
#tuner-name i {
transition: transform 0.3s ease;
}
#tuner-name i.rotated {
transform: rotate(180deg);
}
h1#tuner-name:hover { h1#tuner-name:hover {
color: var(--color-main-bright); color: var(--color-main-bright);
} }
@@ -75,9 +84,13 @@ label {
} }
.canvas-container { .canvas-container {
width: 100%; width: calc(100% - 20px);
height: 175px; height: 140px;
overflow: hidden; overflow: hidden;
border-radius: 15px;
margin-left: 10px;
margin-right: 10px;
position: relative;
} }
#data-ant { #data-ant {

9
web/css/libs/fontawesome.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -4,8 +4,12 @@
<title><%= tunerName %> - FM-DX Webserver</title> <title><%= tunerName %> - FM-DX Webserver</title>
<link href="css/entry.css" type="text/css" rel="stylesheet"> <link href="css/entry.css" type="text/css" rel="stylesheet">
<link href="css/flags.min.css" type="text/css" rel="stylesheet"> <link href="css/flags.min.css" type="text/css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet"> <link href="css/libs/fontawesome.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="js/libs/jquery.min.js"></script>
<script src="js/libs/chart.umd.min.js"></script>
<script src="js/libs/luxon.min.js"></script>
<script src="js/libs/chartjs-adapter-luxon.umd.min.js"></script>
<script src="js/libs/chartjs-plugin-streaming.min.js"></script>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -16,7 +20,7 @@
<meta property="og:description" content="Server description: <%= tunerDescMeta %>."> <meta property="og:description" content="Server description: <%= tunerDescMeta %>.">
<script src="js/init.js"></script> <script src="js/init.js"></script>
<!-- 3LAS Scripts for Audio streaming --> <!-- 3LAS Scripts for Audio streaming -->
<script src="js/3las/util/3las.helpers.js"></script> <script src="js/3las/util/3las.helpers.js"></script>
<script src="js/3las/util/3las.logging.js"></script> <script src="js/3las/util/3las.logging.js"></script>
@@ -39,7 +43,7 @@
<div class="wrapper-outer dashboard-panel" style="padding-top: 20px; z-index: 10; position: relative;"> <div class="wrapper-outer dashboard-panel" style="padding-top: 20px; z-index: 10; position: relative;">
<div class="panel-100-real m-0 flex-container bg-phone flex-phone-column" style="min-height: 64px; max-width: 1160px; margin-top: 10px;align-items: center; justify-content: space-between; padding-left: 20px;padding-right: 10px;"> <div class="panel-100-real m-0 flex-container bg-phone flex-phone-column" style="min-height: 64px; max-width: 1160px; margin-top: 10px;align-items: center; justify-content: space-between; padding-left: 20px;padding-right: 10px;">
<h1 id="tuner-name" class="text-left flex-container flex-phone flex-center" style="padding-bottom: 3px; padding-right: 5px;height: 64px;"> <h1 id="tuner-name" class="text-left flex-container flex-phone flex-center" style="padding-bottom: 3px; padding-right: 5px;height: 64px;">
<span class="text-200-px" style="max-width: 450px;"><%= tunerName %></span> <i class="fa-solid fa-chevron-down p-left-10" style="font-size: 15px;"></i> <span class="text-200-px" style="max-width: 450px;padding-right: 10px;"><%= tunerName %></span> <i class="fa-solid fa-chevron-down" style="font-size: 15px;"></i>
</h1> </h1>
<% if(!publicTuner || tunerLock) { %> <% if(!publicTuner || tunerLock) { %>
<div class="tuner-status p-10 color-3"> <div class="tuner-status p-10 color-3">
@@ -109,27 +113,16 @@
<div style="width: 1px;background: var(--color-2);" class="m-10 hide-phone"></div> <div style="width: 1px;background: var(--color-2);" class="m-10 hide-phone"></div>
<div> <div>
<% const presets = [1, 2, 3, 4]; %>
<div style="height: 64px;" class="flex-center flex-phone"> <div style="height: 64px;" class="flex-center flex-phone">
<button class="no-bg color-4 hover-brighten" id="preset1" style="padding: 6px; width: 64px; min-width: 64px;"> <% presets.forEach(preset => { %>
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br> <button class="no-bg color-4 hover-brighten" id="preset<%= preset %>" style="padding: 6px; width: 64px; min-width: 64px;">
<span style="font-size: 10px; color: var(--color-text);" id="preset1-text"></span> <i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
</button> <span style="font-size: 10px; color: var(--color-text);" id="preset<%= preset %>-text"></span>
</button>
<button class="no-bg color-4 hover-brighten" id="preset2" style="padding: 6px; width: 64px; min-width: 64px;"> <% }); %>
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br> </div>
<span style="font-size: 10px; color: var(--color-text);" id="preset2-text"></span>
</button>
<button class="no-bg color-4 hover-brighten" id="preset3" style="padding: 6px; width: 64px; min-width: 64px;">
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
<span style="font-size: 10px; color: var(--color-text);" id="preset3-text"></span>
</button>
<button class="no-bg color-4 hover-brighten" id="preset4" style="padding: 6px; width: 64px; min-width: 64px;">
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
<span style="font-size: 10px; color: var(--color-text);" id="preset4-text"></span>
</button>
</div>
<div class="flex-container flex-phone" style="align-items: center; height: 64px;"> <div class="flex-container flex-phone" style="align-items: center; height: 64px;">
<div class="color-3 m-10 text-medium"> <div class="color-3 m-10 text-medium">
@@ -440,11 +433,6 @@
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'RDS PS Underscores', id: 'ps-underscores'}) %> <%- include('_components', {component: 'checkbox', cssClass: '', label: 'RDS PS Underscores', id: 'ps-underscores'}) %>
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Imperial units', id: 'imperial-units'}) %> <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Imperial units', id: 'imperial-units'}) %>
<div class="form-group bottom-20 hide-desktop" style="float: none;">
<label for="users-online"><i class="fa-solid fa-user"></i> Users online</label>
<span class="users-online" name="users-online">0</span>
</div>
<% if (isAdminAuthenticated) { %> <% if (isAdminAuthenticated) { %>
<p class="color-3">You are logged in as an adminstrator.</p> <p class="color-3">You are logged in as an adminstrator.</p>
<div class="admin-quick-dashboard"> <div class="admin-quick-dashboard">

View File

@@ -46,3 +46,11 @@ function tuneTo(freq) {
function resetRDS() { function resetRDS() {
socket.send("T0"); socket.send("T0");
} }
function getCurrentFreq() {
currentFreq = $('#data-frequency').text();
currentFreq = parseFloat(currentFreq).toFixed(3);
currentFreq = parseFloat(currentFreq);
return currentFreq;
}

View File

@@ -59,7 +59,7 @@ $(document).ready(function() {
chatNicknameSave.click(function() { chatNicknameSave.click(function() {
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`; const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`;
localStorage.setItem('nickname', currentNickname); localStorage.setItem('nickname', currentNickname);
savedNickname = currentNickname; // Update the savedNickname variable savedNickname = currentNickname;
chatIdentityNickname.text(savedNickname); chatIdentityNickname.text(savedNickname);
chatNicknameInput.blur(); chatNicknameInput.blur();
}); });

View File

@@ -1,9 +1,9 @@
var currentDate = new Date('Feb 16, 2025 15:00:00'); var currentDate = new Date('Feb 23, 2025 15:30:00');
var day = currentDate.getDate(); var day = currentDate.getDate();
var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1 var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1
var year = currentDate.getFullYear(); var year = currentDate.getFullYear();
var formattedDate = day + '/' + month + '/' + year; var formattedDate = day + '/' + month + '/' + year;
var currentVersion = 'v1.3.5 [' + formattedDate + ']'; var currentVersion = 'v1.3.6 [' + formattedDate + ']';
getInitialSettings(); getInitialSettings();
removeUrlParameters(); removeUrlParameters();

20
web/js/libs/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-luxon v1.3.1
* https://www.chartjs.org
* (c) 2023 chartjs-adapter-luxon Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));

File diff suppressed because one or more lines are too long

2
web/js/libs/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
web/js/libs/luxon.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,8 +3,8 @@
var parsedData, signalChart, previousFreq; var parsedData, signalChart, previousFreq;
var signalData = [];
var data = []; var data = [];
var signalData = [];
let updateCounter = 0; let updateCounter = 0;
let messageCounter = 0; // Count for WebSocket data length returning 0 let messageCounter = 0; // Count for WebSocket data length returning 0
let messageData = 800; // Initial value anything above 0 let messageData = 800; // Initial value anything above 0
@@ -32,7 +32,7 @@ const usa_programmes = [
const rdsMode = localStorage.getItem('rdsMode'); const rdsMode = localStorage.getItem('rdsMode');
$(document).ready(function () { $(document).ready(function () {
var canvas = $('#signal-canvas')[0]; const signalToggle = $("#signal-units-toggle");
var $panel = $('.admin-quick-dashboard'); var $panel = $('.admin-quick-dashboard');
var panelWidth = $panel.outerWidth(); var panelWidth = $panel.outerWidth();
@@ -47,12 +47,7 @@ $(document).ready(function () {
$panel.css('left', -panelWidth); $panel.css('left', -panelWidth);
} }
}); });
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
// Start updating the canvas
initCanvas();
fillPresets(); fillPresets();
signalToggle.on("change", function () { signalToggle.on("change", function () {
@@ -205,7 +200,6 @@ $(document).ready(function () {
$(freqContainer).on("click", function () { $(freqContainer).on("click", function () {
textInput.focus(); textInput.focus();
}); });
initTooltips();
//FMLIST logging //FMLIST logging
$('.popup-content').on('click', function(event) { $('.popup-content').on('click', function(event) {
@@ -273,7 +267,9 @@ $(document).ready(function () {
}); });
} }
}); });
initCanvas();
initTooltips();
}); });
function getServerTime() { function getServerTime() {
@@ -294,29 +290,21 @@ function getServerTime() {
const serverOptions = { const serverOptions = {
...options, ...options,
timeZone: 'Etc/UTC' // Add timeZone only for server time timeZone: 'Etc/UTC'
}; };
const formattedServerTime = new Date(serverTimeUtc).toLocaleString(navigator.language ? navigator.language : 'en-US', serverOptions); const formattedServerTime = new Date(serverTimeUtc).toLocaleString(navigator.language ? navigator.language : 'en-US', serverOptions);
$("#server-time").text(formattedServerTime); $("#server-time").text(formattedServerTime);
// Get and format user's local time directly without specifying timeZone:
const localTime = new Date();
const formattedLocalTime = new Date(localTime).toLocaleString(navigator.language ? navigator.language : 'en-US', options);
// Display client time:
$("#client-time").text(formattedLocalTime);
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
console.error("Error fetching server time:", errorThrown); console.error("Error fetching server time:", errorThrown);
// Handle error gracefully (e.g., display a fallback message)
} }
}); });
} }
function sendPingRequest() { function sendPingRequest() {
const timeoutDuration = 15000; // Ping response can become buggy if it exceeds 20 seconds const timeoutDuration = 5000;
const startTime = new Date().getTime(); const startTime = new Date().getTime();
const fetchWithTimeout = (url, options, timeout = timeoutDuration) => { const fetchWithTimeout = (url, options, timeout = timeoutDuration) => {
@@ -407,7 +395,6 @@ function sendPingRequest() {
} }
} }
// Automatic UI resume on WebSocket reconnect
function handleWebSocketMessage(event) { function handleWebSocketMessage(event) {
if (event.data == 'KICK') { if (event.data == 'KICK') {
console.log('Kick initiated.') console.log('Kick initiated.')
@@ -429,151 +416,161 @@ function handleWebSocketMessage(event) {
// Attach the message handler // Attach the message handler
socket.onmessage = handleWebSocketMessage; socket.onmessage = handleWebSocketMessage;
function initCanvas(parsedData) { const signalBuffer = [];
signalToggle = $("#signal-units-toggle");
// Check if signalChart is already initialized
if (!signalChart) {
const canvas = $('#signal-canvas')[0];
const context = canvas.getContext('2d');
const maxDataPoints = 300;
const pointWidth = (canvas.width - 80) / maxDataPoints;
signalChart = {
canvas,
context,
parsedData,
maxDataPoints,
pointWidth,
color2: null,
color3: null,
color4: null,
signalUnit: localStorage.getItem('signalUnit'),
offset: 0,
};
switch(signalChart.signalUnit) {
case 'dbuv': signalChart.offset = 11.25; break;
case 'dbm': signalChart.offset = 120; break;
default: signalChart.offset = 0;
}
// Initialize colors and signal unit
updateChartSettings(signalChart);
// Periodically check for color and signal unit updates
setInterval(() => {
updateChartSettings(signalChart);
}, 1000); // Check every 1 second
}
updateCanvas(parsedData, signalChart);
}
function updateChartSettings(signalChart) { function initCanvas() {
// Update colors const ctx = document.getElementById("signal-canvas").getContext("2d");
const newColor2 = getComputedStyle(document.documentElement).getPropertyValue('--color-2').trim();
const newColor3 = getComputedStyle(document.documentElement).getPropertyValue('--color-3').trim();
const newColor4 = getComputedStyle(document.documentElement).getPropertyValue('--color-4').trim();
if (newColor2 !== signalChart.color2 || newColor4 !== signalChart.color4) {
signalChart.color2 = newColor2;
signalChart.color3 = newColor3;
signalChart.color4 = newColor4;
}
// Update signal unit
const newSignalUnit = localStorage.getItem('signalUnit');
if (newSignalUnit !== signalChart.signalUnit) {
signalChart.signalUnit = newSignalUnit;
// Adjust the offset based on the new signal unit
switch(newSignalUnit) {
case 'dbuv': signalChart.offset = 11.25; break;
case 'dbm': signalChart.offset = 120; break;
default: signalChart.offset = 0;
}
}
}
function updateCanvas(parsedData, signalChart) { window.signalChart = new Chart(ctx, {
const { context, canvas, maxDataPoints, pointWidth, color2, color3, color4, offset } = signalChart; type: "line",
data: {
if (data.length > maxDataPoints) { datasets: [{
data = data.slice(data.length - maxDataPoints); label: "Signal Strength",
} borderColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-4").trim(),
borderWidth: 2,
const actualLowestValue = Math.min(...data); fill: {
const actualHighestValue = Math.max(...data); target: 'start'
const zoomMinValue = actualLowestValue - ((actualHighestValue - actualLowestValue) / 2); },
const zoomMaxValue = actualHighestValue + ((actualHighestValue - actualLowestValue) / 2); backgroundColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-1-transparent").trim(),
const zoomAvgValue = (zoomMaxValue - zoomMinValue) / 2 + zoomMinValue; tension: 0.6,
data: []
// Clear the canvas }]
context.clearRect(0, 0, canvas.width, canvas.height); },
context.beginPath(); options: {
layout: {
const startingIndex = Math.max(0, data.length - maxDataPoints); padding: {
left: -10,
for (let i = startingIndex; i < data.length; i++) { right: -10,
const x = canvas.width - (data.length - i) * pointWidth - 40; bottom: -10
const y = canvas.height - (data[i] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue)); },
},
if (i === startingIndex) { responsive: true,
context.moveTo(x, y); maintainAspectRatio: false,
} else { elements: {
const prevX = canvas.width - (data.length - i + 1) * pointWidth - 40; point: { radius: 0 },
const prevY = canvas.height - (data[i - 1] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue)); },
const interpolatedX = (x + prevX) / 2; scales: {
const interpolatedY = (y + prevY) / 2; x: {
type: "realtime",
context.quadraticCurveTo(prevX, prevY, interpolatedX, interpolatedY); ticks: { display: false },
} border: { display: false },
} grid: { display: false, borderWidth: 0, borderColor: "transparent" },
realtime: {
context.strokeStyle = color4; duration: 30000,
context.lineWidth = 2; refresh: 75,
context.stroke(); delay: 150,
onRefresh: (chart) => {
// Draw horizontal lines for lowest, highest, and average values if (!chart?.data?.datasets || parsedData?.sig === undefined) return;
context.strokeStyle = color3;
context.lineWidth = 1; signalBuffer.push(parsedData.sig);
if (signalBuffer.length > 8) {
// Draw the lowest value line signalBuffer.shift();
const lowestY = canvas.height - (zoomMinValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue)); }
context.beginPath(); const avgSignal = signalBuffer.reduce((sum, val) => sum + val, 0) / signalBuffer.length;
context.moveTo(40, lowestY - 18);
context.lineTo(canvas.width - 40, lowestY - 18); chart.data.datasets[0].data.push({
context.stroke(); x: Date.now(),
y: avgSignal
// Draw the highest value line });
const highestY = canvas.height - (zoomMaxValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue)); }
context.beginPath(); }
context.moveTo(40, highestY + 10); },
context.lineTo(canvas.width - 40, highestY + 10); y: {
context.stroke(); beginAtZero: false,
grace: 0.25,
const avgY = canvas.height / 2; border: { display: false },
context.beginPath(); ticks: {
context.moveTo(40, avgY - 7); maxTicksLimit: 3,
context.lineTo(canvas.width - 40, avgY - 7); display: false // Hide default labels
context.stroke(); },
grid: {
// Label the lines with their values display: false, // Hide default grid lines
context.fillStyle = color4; },
context.font = '12px Titillium Web'; },
y2: {
context.textAlign = 'right'; position: 'right', // Position on the right side
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, 35, lowestY - 14); beginAtZero: false,
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, 35, highestY + 14); grace: 0.25,
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, 35, avgY - 3); border: { display: false },
ticks: {
context.textAlign = 'left'; maxTicksLimit: 3,
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, canvas.width - 35, lowestY - 14); display: false // Hide default labels for the right axis
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, canvas.width - 35, highestY + 14); },
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, canvas.width - 35, avgY - 3); grid: {
display: false, // No grid for right axis
setTimeout(() => { }
requestAnimationFrame(() => updateCanvas(parsedData, signalChart)); }
}, 1000 / 15); },
plugins: {
legend: { display: false },
tooltip: { enabled: false }
}
},
plugins: [{
id: 'customYAxisLabels',
afterDraw: (chart) => {
const { ctx, scales, chartArea } = chart;
const yAxis = scales.y;
const y2Axis = scales.y2;
const gridLineColor = getComputedStyle(document.documentElement).getPropertyValue("--color-2-transparent").trim(); // Grid color using CSS variable
const textColor = getComputedStyle(document.documentElement).getPropertyValue("--color-3-transparent").trim(); // Use the same color for text labels
ctx.save();
ctx.font = "12px Titillium Web";
ctx.fillStyle = textColor;
ctx.textAlign = "center";
const leftX = yAxis.left + 20;
const rightX = y2Axis.right - 20;
const offset = 10;
yAxis.ticks.forEach((tick, index) => {
const y = yAxis.getPixelForValue(tick.value);
var adjustedY = Math.max(yAxis.top + 13, Math.min(y, yAxis.bottom - 6));
const isMiddleTick = index === Math.floor(yAxis.ticks.length / 2);
let adjustedTickValue;
switch(localStorage.getItem("signalUnit")) {
case "dbuv": adjustedTickValue = tick.value - 11.25; break;
case "dbm": adjustedTickValue = tick.value - -120; break;
default: adjustedTickValue = tick.value; break;
}
if (isMiddleTick) { adjustedY += 3; }
ctx.textAlign = 'right';
ctx.fillText(adjustedTickValue.toFixed(1), leftX + 25, adjustedY);
ctx.textAlign = 'left';
ctx.fillText(adjustedTickValue.toFixed(1), rightX - 25, adjustedY); // Right side
});
const gridLineWidth = 0.5; // Make the lines thinner to avoid overlapping text
const adjustedGridTop = chartArea.top + offset;
const adjustedGridBottom = chartArea.bottom - offset;
const middleY = chartArea.top + chartArea.height / 2;
const padding = 45; // 30px inward on both sides
// Helper function to draw a horizontal line
function drawGridLine(y) {
ctx.beginPath();
ctx.moveTo(chartArea.left + padding, y);
ctx.lineTo(chartArea.right - padding, y);
ctx.strokeStyle = gridLineColor;
ctx.lineWidth = gridLineWidth;
ctx.stroke();
}
// Draw the three horizontal grid lines
drawGridLine(adjustedGridTop);
drawGridLine(adjustedGridBottom);
drawGridLine(middleY);
ctx.restore();
}
}]
});
} }
let reconnectTimer = null; let reconnectTimer = null;
@@ -641,14 +638,6 @@ function processString(string, errors) {
return output; return output;
} }
function getCurrentFreq() {
currentFreq = $('#data-frequency').text();
currentFreq = parseFloat(currentFreq).toFixed(3);
currentFreq = parseFloat(currentFreq);
return currentFreq;
}
function checkKey(e) { function checkKey(e) {
e = e || window.event; e = e || window.event;
@@ -899,14 +888,12 @@ function throttle(fn, wait) {
return wrapper; return wrapper;
} }
// Utility function to update element's text if changed
function updateTextIfChanged($element, newText) { function updateTextIfChanged($element, newText) {
if ($element.text() !== newText) { if ($element.text() !== newText) {
$element.text(newText); $element.text(newText);
} }
} }
// Utility function to update element's HTML content if changed
function updateHtmlIfChanged($element, newHtml) { function updateHtmlIfChanged($element, newHtml) {
if ($element.html() !== newHtml) { if ($element.html() !== newHtml) {
$element.html(newHtml); $element.html(newHtml);
@@ -1005,7 +992,7 @@ const updateDataElements = throttle(function(parsedData) {
$dataRt1.attr('aria-label', parsedData.rt1); $dataRt1.attr('aria-label', parsedData.rt1);
$('#users-online-container').attr("aria-label", "Online users: " + parsedData.users); $('#users-online-container').attr("aria-label", "Online users: " + parsedData.users);
} }
}, 100); // Update at most once every 100 milliseconds }, 75); // Update at most once every 100 milliseconds
let isEventListenerAdded = false; let isEventListenerAdded = false;
@@ -1114,9 +1101,12 @@ function showTunerDescription() {
parentDiv.css("border-radius", ""); parentDiv.css("border-radius", "");
} }
}); });
$("#tuner-name i").toggleClass("rotated");
if ($(window).width() < 768) { if ($(window).width() < 768) {
$('.dashboard-panel-plugin-list').slideToggle(300); $('.dashboard-panel-plugin-list').slideToggle(300);
$('#users-online-container').slideToggle(300);
$('.chatbutton').slideToggle(300); $('.chatbutton').slideToggle(300);
$('#settings').slideToggle(300); $('#settings').slideToggle(300);
} }

View File

@@ -1,8 +1,22 @@
$.getScript('./js/api.js'); function loadScript(src) {
$.getScript('./js/main.js'); return new Promise((resolve, reject) => {
$.getScript('./js/dropdown.js'); const script = document.createElement('script');
$.getScript('./js/modal.js'); script.src = src;
$.getScript('./js/settings.js'); script.onload = resolve;
$.getScript('./js/chat.js'); script.onerror = reject;
$.getScript('./js/toast.js'); document.head.appendChild(script);
$.getScript('./js/plugins.js'); });
}
async function loadScriptsInOrder() {
await loadScript('./js/api.js');
await loadScript('./js/main.js');
await loadScript('./js/dropdown.js');
await loadScript('./js/modal.js');
await loadScript('./js/settings.js');
await loadScript('./js/chat.js');
await loadScript('./js/toast.js');
await loadScript('./js/plugins.js');
}
loadScriptsInOrder();