1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +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) => {
console.error("Error fetching location data:", err);
consoleCmd.logError("Error fetching location data:", err.code);
callback("User allowed");
});
}
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);
let bannedASCache = { data: null, timestamp: 0 };
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 = "";
banResponse.on("data", (chunk) => {
@@ -103,39 +107,60 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
banResponse.on("end", () => {
try {
const bannedAS = JSON.parse(banData).banned_as || [];
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");
bannedASCache = { data: bannedAS, timestamp: now };
callback(null, bannedAS);
} catch (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);
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) {
const secondsInMinute = 60;

View File

@@ -469,11 +469,9 @@ wss.on('connection', (ws, request) => {
});
ws.on('close', (code, reason) => {
if (clientIp !== '::ffff:127.0.0.1' ||
(request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') ||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
currentUsers--;
}
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() !== '')) {
currentUsers--;
}
dataHandler.showOnlineUsers(currentUsers);
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
async function loadUsStatesGeoJson() {
if (!usStatesGeoJson) {
const response = await fetch(usStatesGeoJsonUrl);
usStatesGeoJson = await response.json();
try {
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;
return fetch(url, {
redirect: 'manual'
})
.then(response => response.json())
.then(async (data) => {
cachedData[freq] = data;
await loadUsStatesGeoJson();
return processData(data, piCode, rdsPs);
})
.catch(error => {
console.error("Error fetching data:", error);
});
try {
const response = await fetch(url, { redirect: 'manual' });
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const data = await response.json();
cachedData[freq] = data;
await loadUsStatesGeoJson();
return processData(data, piCode, rdsPs);
} catch (error) {
console.error("Error fetching data:", error);
return null; // Return null to indicate failure
}
}
async function processData(data, piCode, rdsPs) {
@@ -182,29 +187,28 @@ function checkEs() {
let esSwitch = false;
if (now - esSwitchCache.lastCheck < esFetchInterval) {
esSwitch = 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 esSwitchCache.esSwitch;
}
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) {

View File

@@ -22,6 +22,15 @@ h1#tuner-name {
transition: 0.3s ease color;
}
#tuner-name i {
transition: transform 0.3s ease;
}
#tuner-name i.rotated {
transform: rotate(180deg);
}
h1#tuner-name:hover {
color: var(--color-main-bright);
}
@@ -75,9 +84,13 @@ label {
}
.canvas-container {
width: 100%;
height: 175px;
width: calc(100% - 20px);
height: 140px;
overflow: hidden;
border-radius: 15px;
margin-left: 10px;
margin-right: 10px;
position: relative;
}
#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>
<link href="css/entry.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">
<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>
<link href="css/libs/fontawesome.css" type="text/css" rel="stylesheet">
<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" />
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -16,7 +20,7 @@
<meta property="og:description" content="Server description: <%= tunerDescMeta %>.">
<script src="js/init.js"></script>
<!-- 3LAS Scripts for Audio streaming -->
<script src="js/3las/util/3las.helpers.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="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;">
<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>
<% if(!publicTuner || tunerLock) { %>
<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>
<% const presets = [1, 2, 3, 4]; %>
<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;">
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
<span style="font-size: 10px; color: var(--color-text);" id="preset1-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>
<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>
<% presets.forEach(preset => { %>
<button class="no-bg color-4 hover-brighten" id="preset<%= preset %>" 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="preset<%= preset %>-text"></span>
</button>
<% }); %>
</div>
<div class="flex-container flex-phone" style="align-items: center; height: 64px;">
<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: '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) { %>
<p class="color-3">You are logged in as an adminstrator.</p>
<div class="admin-quick-dashboard">

View File

@@ -46,3 +46,11 @@ function tuneTo(freq) {
function resetRDS() {
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() {
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`;
localStorage.setItem('nickname', currentNickname);
savedNickname = currentNickname; // Update the savedNickname variable
savedNickname = currentNickname;
chatIdentityNickname.text(savedNickname);
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 month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1
var year = currentDate.getFullYear();
var formattedDate = day + '/' + month + '/' + year;
var currentVersion = 'v1.3.5 [' + formattedDate + ']';
var currentVersion = 'v1.3.6 [' + formattedDate + ']';
getInitialSettings();
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 signalData = [];
var data = [];
var signalData = [];
let updateCounter = 0;
let messageCounter = 0; // Count for WebSocket data length returning 0
let messageData = 800; // Initial value anything above 0
@@ -32,7 +32,7 @@ const usa_programmes = [
const rdsMode = localStorage.getItem('rdsMode');
$(document).ready(function () {
var canvas = $('#signal-canvas')[0];
const signalToggle = $("#signal-units-toggle");
var $panel = $('.admin-quick-dashboard');
var panelWidth = $panel.outerWidth();
@@ -47,12 +47,7 @@ $(document).ready(function () {
$panel.css('left', -panelWidth);
}
});
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
// Start updating the canvas
initCanvas();
fillPresets();
signalToggle.on("change", function () {
@@ -205,7 +200,6 @@ $(document).ready(function () {
$(freqContainer).on("click", function () {
textInput.focus();
});
initTooltips();
//FMLIST logging
$('.popup-content').on('click', function(event) {
@@ -273,7 +267,9 @@ $(document).ready(function () {
});
}
});
initCanvas();
initTooltips();
});
function getServerTime() {
@@ -294,29 +290,21 @@ function getServerTime() {
const serverOptions = {
...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);
$("#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) {
console.error("Error fetching server time:", errorThrown);
// Handle error gracefully (e.g., display a fallback message)
}
});
}
function sendPingRequest() {
const timeoutDuration = 15000; // Ping response can become buggy if it exceeds 20 seconds
const timeoutDuration = 5000;
const startTime = new Date().getTime();
const fetchWithTimeout = (url, options, timeout = timeoutDuration) => {
@@ -407,7 +395,6 @@ function sendPingRequest() {
}
}
// Automatic UI resume on WebSocket reconnect
function handleWebSocketMessage(event) {
if (event.data == 'KICK') {
console.log('Kick initiated.')
@@ -429,151 +416,161 @@ function handleWebSocketMessage(event) {
// Attach the message handler
socket.onmessage = handleWebSocketMessage;
function initCanvas(parsedData) {
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);
}
const signalBuffer = [];
function updateChartSettings(signalChart) {
// Update colors
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 initCanvas() {
const ctx = document.getElementById("signal-canvas").getContext("2d");
function updateCanvas(parsedData, signalChart) {
const { context, canvas, maxDataPoints, pointWidth, color2, color3, color4, offset } = signalChart;
if (data.length > maxDataPoints) {
data = data.slice(data.length - maxDataPoints);
}
const actualLowestValue = Math.min(...data);
const actualHighestValue = Math.max(...data);
const zoomMinValue = actualLowestValue - ((actualHighestValue - actualLowestValue) / 2);
const zoomMaxValue = actualHighestValue + ((actualHighestValue - actualLowestValue) / 2);
const zoomAvgValue = (zoomMaxValue - zoomMinValue) / 2 + zoomMinValue;
// Clear the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
const startingIndex = Math.max(0, data.length - maxDataPoints);
for (let i = startingIndex; i < data.length; i++) {
const x = canvas.width - (data.length - i) * pointWidth - 40;
const y = canvas.height - (data[i] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
if (i === startingIndex) {
context.moveTo(x, y);
} else {
const prevX = canvas.width - (data.length - i + 1) * pointWidth - 40;
const prevY = canvas.height - (data[i - 1] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
const interpolatedX = (x + prevX) / 2;
const interpolatedY = (y + prevY) / 2;
context.quadraticCurveTo(prevX, prevY, interpolatedX, interpolatedY);
}
}
context.strokeStyle = color4;
context.lineWidth = 2;
context.stroke();
// Draw horizontal lines for lowest, highest, and average values
context.strokeStyle = color3;
context.lineWidth = 1;
// Draw the lowest value line
const lowestY = canvas.height - (zoomMinValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
context.beginPath();
context.moveTo(40, lowestY - 18);
context.lineTo(canvas.width - 40, lowestY - 18);
context.stroke();
// 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);
context.stroke();
const avgY = canvas.height / 2;
context.beginPath();
context.moveTo(40, avgY - 7);
context.lineTo(canvas.width - 40, avgY - 7);
context.stroke();
// Label the lines with their values
context.fillStyle = color4;
context.font = '12px Titillium Web';
context.textAlign = 'right';
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, 35, lowestY - 14);
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, 35, highestY + 14);
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, 35, avgY - 3);
context.textAlign = 'left';
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, canvas.width - 35, lowestY - 14);
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, canvas.width - 35, highestY + 14);
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, canvas.width - 35, avgY - 3);
setTimeout(() => {
requestAnimationFrame(() => updateCanvas(parsedData, signalChart));
}, 1000 / 15);
window.signalChart = new Chart(ctx, {
type: "line",
data: {
datasets: [{
label: "Signal Strength",
borderColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-4").trim(),
borderWidth: 2,
fill: {
target: 'start'
},
backgroundColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-1-transparent").trim(),
tension: 0.6,
data: []
}]
},
options: {
layout: {
padding: {
left: -10,
right: -10,
bottom: -10
},
},
responsive: true,
maintainAspectRatio: false,
elements: {
point: { radius: 0 },
},
scales: {
x: {
type: "realtime",
ticks: { display: false },
border: { display: false },
grid: { display: false, borderWidth: 0, borderColor: "transparent" },
realtime: {
duration: 30000,
refresh: 75,
delay: 150,
onRefresh: (chart) => {
if (!chart?.data?.datasets || parsedData?.sig === undefined) return;
signalBuffer.push(parsedData.sig);
if (signalBuffer.length > 8) {
signalBuffer.shift();
}
const avgSignal = signalBuffer.reduce((sum, val) => sum + val, 0) / signalBuffer.length;
chart.data.datasets[0].data.push({
x: Date.now(),
y: avgSignal
});
}
}
},
y: {
beginAtZero: false,
grace: 0.25,
border: { display: false },
ticks: {
maxTicksLimit: 3,
display: false // Hide default labels
},
grid: {
display: false, // Hide default grid lines
},
},
y2: {
position: 'right', // Position on the right side
beginAtZero: false,
grace: 0.25,
border: { display: false },
ticks: {
maxTicksLimit: 3,
display: false // Hide default labels for the right axis
},
grid: {
display: false, // No grid for right axis
}
}
},
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;
@@ -641,14 +638,6 @@ function processString(string, errors) {
return output;
}
function getCurrentFreq() {
currentFreq = $('#data-frequency').text();
currentFreq = parseFloat(currentFreq).toFixed(3);
currentFreq = parseFloat(currentFreq);
return currentFreq;
}
function checkKey(e) {
e = e || window.event;
@@ -899,14 +888,12 @@ function throttle(fn, wait) {
return wrapper;
}
// Utility function to update element's text if changed
function updateTextIfChanged($element, newText) {
if ($element.text() !== newText) {
$element.text(newText);
}
}
// Utility function to update element's HTML content if changed
function updateHtmlIfChanged($element, newHtml) {
if ($element.html() !== newHtml) {
$element.html(newHtml);
@@ -1005,7 +992,7 @@ const updateDataElements = throttle(function(parsedData) {
$dataRt1.attr('aria-label', parsedData.rt1);
$('#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;
@@ -1114,9 +1101,12 @@ function showTunerDescription() {
parentDiv.css("border-radius", "");
}
});
$("#tuner-name i").toggleClass("rotated");
if ($(window).width() < 768) {
$('.dashboard-panel-plugin-list').slideToggle(300);
$('#users-online-container').slideToggle(300);
$('.chatbutton').slideToggle(300);
$('#settings').slideToggle(300);
}

View File

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