You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 14:11:59 +01:00
new chat window, bugfixes, component update
This commit is contained in:
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fm-dx-webserver",
|
||||
"version": "1.3.6.1",
|
||||
"version": "1.3.7",
|
||||
"description": "FM DX Webserver",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -12,10 +12,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "1.0.11",
|
||||
"body-parser": "1.20.3",
|
||||
"@mapbox/node-pre-gyp": "2.0.0",
|
||||
"body-parser": "2.2.0",
|
||||
"ejs": "3.1.10",
|
||||
"express": "4.21.2",
|
||||
"express": "5.1.0",
|
||||
"express-session": "1.18.1",
|
||||
"ffmpeg-static": "5.2.0",
|
||||
"http": "0.0.1-security",
|
||||
@@ -23,6 +23,6 @@
|
||||
"koffi": "2.7.2",
|
||||
"net": "1.0.2",
|
||||
"serialport": "12.0.0",
|
||||
"ws": "8.18.0"
|
||||
"ws": "8.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,21 +179,44 @@ router.get('/api', (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
||||
const MAX_ATTEMPTS = 25;
|
||||
const WINDOW_MS = 15 * 60 * 1000;
|
||||
|
||||
const authenticate = (req, res, next) => {
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const now = Date.now();
|
||||
|
||||
if (!loginAttempts[ip]) {
|
||||
loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
} else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) {
|
||||
loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||
}
|
||||
|
||||
if (loginAttempts[ip].count >= MAX_ATTEMPTS) {
|
||||
return res.status(403).json({
|
||||
message: 'Too many login attempts. Please try again later.'
|
||||
});
|
||||
}
|
||||
|
||||
const { password } = req.body;
|
||||
|
||||
// Check if the entered password matches the admin password
|
||||
|
||||
loginAttempts[ip].lastAttempt = now;
|
||||
|
||||
if (password === serverConfig.password.adminPass) {
|
||||
req.session.isAdminAuthenticated = true;
|
||||
req.session.isTuneAuthenticated = true;
|
||||
logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.');
|
||||
logInfo(`User from ${ip} logged in as an administrator.`);
|
||||
loginAttempts[ip].count = 0;
|
||||
next();
|
||||
} else if (password === serverConfig.password.tunePass) {
|
||||
req.session.isAdminAuthenticated = false;
|
||||
req.session.isTuneAuthenticated = true;
|
||||
logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.');
|
||||
logInfo(`User from ${ip} logged in with tune permissions.`);
|
||||
loginAttempts[ip].count = 0;
|
||||
next();
|
||||
} else {
|
||||
loginAttempts[ip].count += 1;
|
||||
res.status(403).json({ message: 'Login failed. Wrong password?' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,6 +132,7 @@ function fetchBannedAS(callback) {
|
||||
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);
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
|
||||
fetchBannedAS((error, bannedAS) => {
|
||||
if (error) {
|
||||
@@ -155,7 +156,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
});
|
||||
|
||||
consoleCmd.logInfo(
|
||||
`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
|
||||
`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
|
||||
);
|
||||
|
||||
callback("User allowed");
|
||||
|
||||
@@ -388,6 +388,7 @@ wss.on('connection', (ws, request) => {
|
||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
||||
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
@@ -509,7 +510,7 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
|
||||
if (code !== 1008) {
|
||||
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
|
||||
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -87,14 +87,11 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
const now = Date.now();
|
||||
freq = parseFloat(freq);
|
||||
|
||||
if (isNaN(freq)) {
|
||||
return;
|
||||
}
|
||||
if (isNaN(freq)) return;
|
||||
if (now - lastFetchTime < fetchInterval
|
||||
|| serverConfig.identification.lat.length < 2
|
||||
|| freq < 87
|
||||
|| (currentPiCode == piCode && currentRdsPs == rdsPs))
|
||||
{
|
||||
|| (currentPiCode == piCode && currentRdsPs == rdsPs)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -107,15 +104,33 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
const url = "https://maps.fmdx.org/api/?freq=" + freq;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { redirect: 'manual' });
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
// Try POST first
|
||||
const postResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ freq }), // You can omit or customize this
|
||||
redirect: 'manual'
|
||||
});
|
||||
|
||||
if (!postResponse.ok) throw new Error(`POST failed: ${postResponse.status}`);
|
||||
const data = await postResponse.json();
|
||||
cachedData[freq] = data;
|
||||
if(serverConfig.webserver.rdsMode == true) await loadUsStatesGeoJson();
|
||||
if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson();
|
||||
return processData(data, piCode, rdsPs);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return null; // Return null to indicate failure
|
||||
} catch (postError) {
|
||||
console.warn("POST failed, trying GET:", postError);
|
||||
|
||||
try {
|
||||
const getResponse = await fetch(url, { redirect: 'manual' });
|
||||
if (!getResponse.ok) throw new Error(`GET failed: ${getResponse.status}`);
|
||||
const data = await getResponse.json();
|
||||
cachedData[freq] = data;
|
||||
if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson();
|
||||
return processData(data, piCode, rdsPs);
|
||||
} catch (getError) {
|
||||
console.error("GET also failed:", getError);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,13 @@ switch (component) {
|
||||
* @param cssClass Custom CSS class if needed
|
||||
*/
|
||||
case 'checkbox':
|
||||
%>
|
||||
<div class="form-group checkbox <%= cssClass %>">
|
||||
<input type="checkbox" tabindex="0" id="<%= id %>" aria-label="<%= label %>">
|
||||
<label for="<%= id %>"><i class="fa-solid fa-toggle-off m-right-10 <%= typeof iconClass !== 'undefined' ? iconClass : '' %>"></i> <%= label %></label>
|
||||
%>
|
||||
<div class="form-group">
|
||||
<div class="switch flex-container flex-phone flex-phone-column flex-phone-center">
|
||||
<input type="checkbox" tabindex="0" id="<%= id %>" aria-label="<%= label %>" />
|
||||
<label for="<%= id %>"></label>
|
||||
<span class="text-smaller text-uppercase text-bold color-4 p-10"><%= label %></span>
|
||||
</div>
|
||||
</div>
|
||||
<%
|
||||
break;
|
||||
|
||||
@@ -308,6 +308,47 @@ input[type="range"]::-moz-range-thumb {
|
||||
|
||||
/* End Toggle Switch */
|
||||
|
||||
.switch {
|
||||
user-select: none;
|
||||
}
|
||||
.switch input[type=checkbox] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.switch input[type=checkbox]:checked + label {
|
||||
background: var(--color-4);
|
||||
}
|
||||
.switch input[type=checkbox]:checked + label::after {
|
||||
left: calc(100% - 4px);
|
||||
transform: translateX(-100%);
|
||||
background-color: var(--color-1);
|
||||
}
|
||||
.switch label {
|
||||
cursor: pointer;
|
||||
min-width: 64px;
|
||||
max-width: 64px;
|
||||
height: 38px;
|
||||
background-color: var(--color-1);
|
||||
transition: 0.35s background-color;
|
||||
display: block;
|
||||
border-radius: 24px;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
border: 2px solid var(--color-3);
|
||||
}
|
||||
.switch label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--color-5);
|
||||
border-radius: 16px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 42px;
|
||||
width: 150px;
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hover-brighten {
|
||||
transition: 0.3s ease background-color;
|
||||
}
|
||||
@@ -302,6 +306,10 @@ table .input-text {
|
||||
.flex-phone-column {
|
||||
flex-flow: column;
|
||||
}
|
||||
.flex-phone-center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hide-phone {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -86,10 +86,6 @@ body.modal-open {
|
||||
background-color: var(--color-main);
|
||||
}
|
||||
|
||||
.modal-panel .flex-container {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.modal-panel h1 {
|
||||
font-size: 42px;
|
||||
}
|
||||
@@ -124,7 +120,6 @@ body.modal-open {
|
||||
|
||||
.modal-panel label {
|
||||
width: 200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-panel-chat {
|
||||
@@ -147,6 +142,37 @@ body.modal-open {
|
||||
border-radius: 15px 15px 0px 0px;
|
||||
}
|
||||
|
||||
/* Popup Windows */
|
||||
.popup-window {
|
||||
width: 500px;
|
||||
background: var(--color-1-transparent);
|
||||
backdrop-filter: blur(5px);
|
||||
position: absolute !important;
|
||||
border-radius: 15px;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--color-1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-3);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
max-width: 24px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.modal-content {
|
||||
min-width: 90% !important;
|
||||
@@ -165,11 +191,9 @@ body.modal-open {
|
||||
.modal-panel {
|
||||
width: 100%;
|
||||
}
|
||||
.modal-panel-chat {
|
||||
height: 510px;
|
||||
}
|
||||
#chat-chatbox {
|
||||
height: 333px !important;
|
||||
|
||||
.popup-window {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
.panel-75 {
|
||||
width: 90%;
|
||||
}
|
||||
.no-bg-phone {
|
||||
background-color: transparent;
|
||||
}
|
||||
.panel-33 h2 {
|
||||
padding: 20px;
|
||||
padding-top: 5px;
|
||||
@@ -81,7 +84,6 @@
|
||||
margin: auto;
|
||||
width: 90%;
|
||||
margin-bottom: 20px;
|
||||
background-color: transparent;
|
||||
}
|
||||
*[class^="panel-"]:not(.no-bg):not(.no-filter):not(#ps-container),
|
||||
.panel-100.w-100::before {
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<link href="css/entry.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/flags.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/libs/fontawesome.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/libs/jquery-ui.min.css" type="text/css" rel="stylesheet">
|
||||
<!--<link href="css/libs/jquery-ui.theme.min.css" type="text/css" rel="stylesheet">-->
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<script src="js/libs/jquery-ui.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>
|
||||
@@ -178,7 +181,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-33 hover-brighten tooltip" id="pi-code-container" data-tooltip="Clicking on the PI code will show the current station on a map.">
|
||||
<div class="panel-33 hover-brighten tooltip no-bg-phone" id="pi-code-container" data-tooltip="Clicking on the PI code will show the current station on a map.">
|
||||
<h2 class="signal-heading">PI CODE</h2>
|
||||
<div class="text-small text-gray highest-signal-container">
|
||||
<span id="data-regular-pi"> </span>
|
||||
@@ -186,12 +189,12 @@
|
||||
<span id="data-pi" class="text-big text-uppercase"></span>
|
||||
</div>
|
||||
|
||||
<div class="panel-33 hover-brighten" id="freq-container">
|
||||
<div class="panel-33 hover-brighten no-bg-phone" id="freq-container">
|
||||
<h2>FREQUENCY</h2>
|
||||
<span id="data-frequency" class="text-big"></span>
|
||||
</div>
|
||||
|
||||
<div class="panel-33">
|
||||
<div class="panel-33 no-bg-phone">
|
||||
<h2 class="signal-heading">SIGNAL</h2>
|
||||
<div class="text-small text-gray highest-signal-container">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
@@ -334,7 +337,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone flex-phone-column">
|
||||
<div class="panel-75 hover-brighten" id="rt-container" style="height: 100px;">
|
||||
<div class="panel-75 hover-brighten no-bg-phone" id="rt-container" style="height: 100px;">
|
||||
<h2 style="margin-top: 4px;">RADIOTEXT</h2>
|
||||
<div id="data-rt0">
|
||||
<span></span>
|
||||
@@ -345,7 +348,7 @@
|
||||
<hr class="hide-desktop">
|
||||
</div>
|
||||
|
||||
<div class="panel-33 hover-brighten tooltip" style="min-height: 91px;"
|
||||
<div class="panel-33 hover-brighten tooltip no-bg-phone" style="min-height: 91px;"
|
||||
data-tooltip="This panel contains the current TX info when RDS is loaded.<br><strong>Clicking on this panel copies the info into the clipboard.</strong>">
|
||||
<div id="data-station-container">
|
||||
<h2 style="margin-top: 0;" class="mb-0">
|
||||
@@ -364,7 +367,7 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-10 no-bg center-phone" style="margin-left: 0; margin-top: 0; margin-right: 0;display:flex;">
|
||||
<div class="panel-100" style="margin-left: 0;">
|
||||
<div class="panel-100 no-bg-phone" style="margin-left: 0;">
|
||||
<h2 class="bottom-10">AF</h2>
|
||||
<div id="af-list" style="text-align: center;">
|
||||
<ul> </ul>
|
||||
@@ -373,7 +376,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="flags-container-phone" class="panel-33">
|
||||
<div id="flags-container-phone" class="panel-33 no-bg-phone">
|
||||
<h2 class="show-phone">
|
||||
<div class="data-pty text-color-default"></div>
|
||||
</h2>
|
||||
@@ -394,6 +397,30 @@
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup-window" id="popup-panel-chat">
|
||||
<div class="flex-container flex-column flex-phone flex-phone-column" style="height: calc(100%);">
|
||||
<div class="popup-header hover-brighten flex-center text-medium-big"><button class="popup-close">✖</button></div>
|
||||
<div class="popup-content text-left flex-container flex-phone flex-column" style="flex: 1;">
|
||||
<div style="text-align: center;white-space-collapse: collapse;">
|
||||
<div class="flex-phone flex-container flex-center top-10">
|
||||
<input type="text" id="chat-nickname" name="chat-nickname" placeholder="Nickname" style="border-radius: 15px 0 0 15px;padding-top:0;padding-bottom:0;border: 2px solid var(--color-4)">
|
||||
<button class="br-0 w-100" style="height: 48px; border-radius: 0 15px 15px 0;margin-left:-3px;" id="chat-nickname-save">Save</button>
|
||||
</div>
|
||||
<p style="margin: 5px;" class="text-small">
|
||||
Current identity: <span style="color: #bada55;" id="chat-admin"></span> <strong id="chat-identity-nickname"></strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="chat-chatbox" class="bg-color-1" style="padding: 10px; overflow-y: auto; flex-grow: 1; min-height: 0; flex-basis: 0; min-height: 120px;"></div>
|
||||
|
||||
<div class="flex-container flex-phone">
|
||||
<input class="bg-color-2" type="text" id="chat-send-message" name="chat-send-message" placeholder="Send message..." style="background-color: var(--color-2);width: 100%;">
|
||||
<button aria-label="Send message" class="chat-send-message-btn br-0" style="width: 80px; height: 48px;"><i class="fa-solid fa-paper-plane"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="myModal" class="modal">
|
||||
<div class="modal-panel">
|
||||
@@ -430,9 +457,11 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'top-25', label: 'Manual decimals', id: 'extended-frequency-range'}) %>
|
||||
<%- 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="auto" style="max-width: 215px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'top-25', label: 'Manual decimals', id: 'extended-frequency-range'}) %>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'RDS PS Underscores', id: 'ps-underscores'}) %>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Imperial units', id: 'imperial-units'}) %>
|
||||
</div>
|
||||
|
||||
<% if (isAdminAuthenticated) { %>
|
||||
<p class="color-3">You are logged in as an adminstrator.</p>
|
||||
@@ -500,29 +529,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-panel-chat">
|
||||
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close chat" tabindex="0"><i class="fa-solid fa-chevron-down"></i></div>
|
||||
<div class="modal-panel-content text-left">
|
||||
<div style="text-align: center;white-space-collapse: collapse;">
|
||||
<div class="flex-phone flex-container flex-center top-10">
|
||||
<input type="text" id="chat-nickname" name="chat-nickname" placeholder="Nickname" style="border-radius: 15px 0 0 15px;padding-top:0;padding-bottom:0;border: 2px solid var(--color-4)">
|
||||
<button class="br-0 w-100" style="height: 48px; border-radius: 0 15px 15px 0;margin-left:-3px;" id="chat-nickname-save">Save</button>
|
||||
</div>
|
||||
<p style="margin: 5px;" class="text-small">
|
||||
Current identity: <span style="color: #bada55;" id="chat-admin"></span> <strong id="chat-identity-nickname"></strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="chat-chatbox" class="bg-color-1" style="height: 258px;padding: 10px;overflow-y: auto;">
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone" style="align-content: stretch;">
|
||||
<input class="bg-color-2" type="text" id="chat-send-message" name="chat-send-message" placeholder="Send message..." style="background-color: var(--color-2);width: 100%;">
|
||||
<button aria-label="Send message" class="chat-send-message-btn br-0" style="width: 80px; height: 48px;"><i class="fa-solid fa-paper-plane"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast-container"></div>
|
||||
|
||||
@@ -34,20 +34,17 @@ function OnConnectivityCallback(isConnected) {
|
||||
}
|
||||
}
|
||||
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
}
|
||||
|
||||
function OnPlayButtonClick(_ev) {
|
||||
const $playbutton = $('.playbutton');
|
||||
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
if (Stream) {
|
||||
console.log("Stopping stream...");
|
||||
shouldReconnect = false;
|
||||
destroyStream();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
|
||||
if (isiOS && 'audioSession' in navigator) {
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = "none";
|
||||
}
|
||||
} else {
|
||||
@@ -56,7 +53,7 @@ function OnPlayButtonClick(_ev) {
|
||||
createStream();
|
||||
Stream.Start();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
|
||||
if (isiOS && 'audioSession' in navigator) {
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = "playback";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,29 @@ $(document).ready(function() {
|
||||
const chatNicknameInput = $('#chat-nickname');
|
||||
const chatNicknameSave = $('#chat-nickname-save');
|
||||
|
||||
$(function () {
|
||||
$("#popup-panel-chat").draggable({
|
||||
handle: ".popup-header"
|
||||
}).resizable({
|
||||
minHeight: 300,
|
||||
minWidth: 250
|
||||
});
|
||||
|
||||
$(".chatbutton").on("click", function () {
|
||||
$("#popup-panel-chat").fadeIn(200, function () {
|
||||
chatMessages.scrollTop(chatMessages[0].scrollHeight);
|
||||
});
|
||||
});
|
||||
|
||||
$("#popup-panel-chat .popup-close").on("click", function () {
|
||||
$("#popup-panel-chat").fadeOut(200);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Function to generate a random string
|
||||
function generateRandomString(length) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const characters = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
@@ -21,7 +41,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
// Load nickname from localStorage on page load
|
||||
let savedNickname = localStorage.getItem('nickname') || `Anonymous User ${generateRandomString(5)}`;
|
||||
let savedNickname = localStorage.getItem('nickname') || `User ${generateRandomString(5)}`;
|
||||
chatNicknameInput.val(savedNickname);
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
|
||||
|
||||
@@ -96,8 +96,6 @@ function populateFields(data, prefix = "") {
|
||||
$element.val(value);
|
||||
}
|
||||
});
|
||||
|
||||
updateIconState();
|
||||
}
|
||||
|
||||
function updateConfigData(data, prefix = "") {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
var currentDate = new Date('Apr 19, 2025 21:30:00');
|
||||
var currentDate = new Date('Apr 22, 2025 21: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.6.1 [' + formattedDate + ']';
|
||||
var currentVersion = 'v1.3.7 [' + formattedDate + ']';
|
||||
|
||||
getInitialSettings();
|
||||
removeUrlParameters();
|
||||
|
||||
@@ -2,7 +2,6 @@ $(document).ready(function() {
|
||||
// Cache jQuery objects for reuse
|
||||
var modal = $("#myModal");
|
||||
var modalPanel = $(".modal-panel");
|
||||
var chatPanel = $(".modal-panel-chat");
|
||||
var openBtn = $("#settings");
|
||||
var closeBtn = $(".closeModal, .closeModalButton");
|
||||
|
||||
@@ -20,7 +19,6 @@ $(document).ready(function() {
|
||||
modal.css("opacity", 0);
|
||||
setTimeout(function() {
|
||||
modal.css("display", "none");
|
||||
modalPanel.add(chatPanel).css("display", "none");
|
||||
$("body").removeClass("modal-open"); // Enable body scrolling
|
||||
}, 300);
|
||||
}
|
||||
@@ -30,10 +28,6 @@ $(document).ready(function() {
|
||||
openModal(modalPanel);
|
||||
});
|
||||
|
||||
$(".chatbutton").on("click", function() {
|
||||
openModal(chatPanel);
|
||||
});
|
||||
|
||||
closeBtn.on("click", closeModal);
|
||||
|
||||
// Close the modal when clicking outside of it
|
||||
|
||||
@@ -166,11 +166,6 @@ $(document).ready(() => {
|
||||
$('.version-string').text(currentVersion);
|
||||
|
||||
setBg();
|
||||
|
||||
// Update icons when the checkbox state changes
|
||||
$('input[type="checkbox"]').change(function() {
|
||||
updateIconState(this);
|
||||
});
|
||||
});
|
||||
|
||||
function getQueryParameter(name) {
|
||||
@@ -178,29 +173,6 @@ function getQueryParameter(name) {
|
||||
return urlParams.get(name);
|
||||
}
|
||||
|
||||
function updateIconState(el) {
|
||||
// If an element is passed, update only that one
|
||||
if (el) {
|
||||
var $checkbox = $(el);
|
||||
var icon = $checkbox.siblings('label').find('i');
|
||||
if ($checkbox.is(':checked')) {
|
||||
icon.removeClass('fa-toggle-off').addClass('fa-toggle-on');
|
||||
} else {
|
||||
icon.removeClass('fa-toggle-on').addClass('fa-toggle-off');
|
||||
}
|
||||
} else {
|
||||
// Otherwise, update all checkboxes
|
||||
$('input[type="checkbox"]').each(function() {
|
||||
var icon = $(this).siblings('label').find('i');
|
||||
if ($(this).is(':checked')) {
|
||||
icon.removeClass('fa-toggle-off').addClass('fa-toggle-on');
|
||||
} else {
|
||||
icon.removeClass('fa-toggle-on').addClass('fa-toggle-off');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(themeName) {
|
||||
const themeColors = themes[themeName];
|
||||
if (themeColors) {
|
||||
|
||||
@@ -20,34 +20,48 @@ $(document).ready(function() {
|
||||
function mapCreate() {
|
||||
if (!(typeof map == "object")) {
|
||||
map = L.map('map', {
|
||||
center: [40,0],
|
||||
center: [40, 0],
|
||||
zoom: 3
|
||||
});
|
||||
} else {
|
||||
map.setZoom(3).panTo([40, 0]);
|
||||
}
|
||||
else {
|
||||
map.setZoom(3).panTo([40,0]);
|
||||
}
|
||||
|
||||
|
||||
L.tileLayer(tilesURL, {
|
||||
attribution: mapAttrib,
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// Check for initial lat/lon values
|
||||
const latVal = parseFloat($('#identification-lat').val());
|
||||
const lonVal = parseFloat($('#identification-lon').val());
|
||||
|
||||
if (!isNaN(latVal) && !isNaN(lonVal)) {
|
||||
const initialLatLng = L.latLng(latVal, lonVal);
|
||||
pin = L.marker(initialLatLng, { riseOnHover: true, draggable: true }).addTo(map);
|
||||
map.setView(initialLatLng, 8); // Optional: Zoom in closer to the pin
|
||||
|
||||
pin.on('dragend', function(ev) {
|
||||
$('#identification-lat').val(ev.target.getLatLng().lat.toFixed(6));
|
||||
$('#identification-lon').val(ev.target.getLatLng().lng.toFixed(6));
|
||||
});
|
||||
}
|
||||
|
||||
map.on('click', function(ev) {
|
||||
$('#identification-lat').val((ev.latlng.lat).toFixed(6));
|
||||
$('#identification-lon').val((ev.latlng.lng).toFixed(6));
|
||||
|
||||
$('#identification-lat').val(ev.latlng.lat.toFixed(6));
|
||||
$('#identification-lon').val(ev.latlng.lng.toFixed(6));
|
||||
|
||||
if (typeof pin == "object") {
|
||||
pin.setLatLng(ev.latlng);
|
||||
} else {
|
||||
pin = L.marker(ev.latlng,{ riseOnHover:true,draggable:true });
|
||||
pin.addTo(map);
|
||||
pin.on('dragend',function(ev) {
|
||||
$('#identification-lat').val((ev.latlng.lat).toFixed(6));
|
||||
$('#identification.lon').val((ev.latlng.lng).toFixed(6));
|
||||
pin = L.marker(ev.latlng, { riseOnHover: true, draggable: true }).addTo(map);
|
||||
pin.on('dragend', function(ev) {
|
||||
$('#identification-lat').val(ev.target.getLatLng().lat.toFixed(6));
|
||||
$('#identification-lon').val(ev.target.getLatLng().lng.toFixed(6));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mapReload();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-100-real p-bottom-20" style="overflow-x: auto;">
|
||||
<h3>Current users</h3>
|
||||
<table class="table-big">
|
||||
@@ -118,7 +119,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-100-real p-bottom-20">
|
||||
<h3>Quick settings</h3>
|
||||
<div class="flex-container flex-center" style="margin: 30px;">
|
||||
@@ -129,6 +132,7 @@
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Tune password', id: 'password-tunePass', password: true}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Admin password', id: 'password-adminPass', password: true}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-100-real p-bottom-20 bottom-20">
|
||||
@@ -223,9 +227,10 @@
|
||||
<p>Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
|
||||
</div>
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Samplerate Offset</h3>
|
||||
<p>Using a negative value could eliminate audio buffering issues during long periods of listening. However, a value that’s too low might increase the buffer over time.</p>
|
||||
<div class="panel-50 p-botom-20">
|
||||
<h3>Sample rate Offset</h3>
|
||||
<p>Using a negative value could eliminate audio buffering issues during long periods of listening. <br>
|
||||
However, a value that’s too low might increase the buffer over time.</p>
|
||||
<p><input class="panel-33 input-text w-100 auto" type="number" style="min-height: 40px; color: var(--color-text); padding: 10px; padding-left: 10px; box-sizing: border-box; border: 2px solid transparent; font-family: 'Titillium Web', sans-serif;" id="audio-samplerateOffset" min="-10" max="10" step="1" value="0" aria-label="Samplerate offset"></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,13 +239,13 @@
|
||||
<div class="panel-full m-0 tab-content no-bg" id="webserver" role="tabpanel">
|
||||
<h2>Webserver settings</h2>
|
||||
<div class="flex-container contains-dropdown">
|
||||
<div class="panel-33 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<div class="panel-50 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<h3>Connection</h3>
|
||||
<p class="text-gray">Leave the IP at 0.0.0.0 unless you explicitly know you have to change it.<br>Don't enter your public IP here.</p>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '0.0.0.0', label: 'Webserver IP', id: 'webserver-webserverIp'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: '8080', label: 'Webserver port', id: 'webserver-webserverPort'}) %><br>
|
||||
</div>
|
||||
<div class="panel-33 p-bottom-20">
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Design</h3>
|
||||
<h4>Background image</h4>
|
||||
<%- include('_components', {component: 'text', cssClass: 'br-15', placeholder: 'Direct image link', label: 'Image link', id: 'webserver-bgImage'}) %><br>
|
||||
@@ -260,21 +265,33 @@
|
||||
]
|
||||
}) %><br>
|
||||
</div>
|
||||
<div class="panel-33 p-bottom-20">
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="panel-100-real p-bottom-20">
|
||||
<h3>Antennas</h3>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', label: 'Antenna switch', id: 'antennas-enabled'}) %><br>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 1', id: 'antennas-ant1-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant A', label: 'Antenna 1 name', id: 'antennas-ant1-name'}) %><br>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 2', id: 'antennas-ant2-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant B', label: 'Antenna 2 name', id: 'antennas-ant2-name'}) %><br>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 3', id: 'antennas-ant3-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant C', label: 'Antenna 3 name', id: 'antennas-ant3-name'}) %><br>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 4', id: 'antennas-ant4-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant D', label: 'Antenna 4 name', id: 'antennas-ant4-name'}) %><br>
|
||||
<div class="flex-container flex-center p-20">
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 1', id: 'antennas-ant1-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant A', label: 'Antenna 1 name', id: 'antennas-ant1-name'}) %><br>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'top-25', label: 'Antenna 2', id: 'antennas-ant2-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant B', label: 'Antenna 2 name', id: 'antennas-ant2-name'}) %><br>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 3', id: 'antennas-ant3-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant C', label: 'Antenna 3 name', id: 'antennas-ant3-name'}) %><br>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone flex-column bottom-20" style="margin-left: 15px; margin-right: 15px;">
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 4', id: 'antennas-ant4-enabled'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant D', label: 'Antenna 4 name', id: 'antennas-ant4-name'}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -303,12 +320,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container p-bottom-20">
|
||||
<div class="panel-33 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<div class="panel-50 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<h3>RDS Mode</h3>
|
||||
<p>You can switch between American (RBDS) / Global (RDS) mode here.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %><br>
|
||||
</div>
|
||||
<div class="panel-33 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<div class="panel-50 p-bottom-20" style="padding-left: 20px; padding-right: 20px;">
|
||||
<h3>Transmitter Search Algorithm</h3>
|
||||
<p>Different modes may help with more accurate transmitter identification depending on your region.</p>
|
||||
<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1',
|
||||
@@ -553,9 +570,9 @@
|
||||
When you become a supporter, you can message the Founders on Discord for your login details.</p>
|
||||
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %><br>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Subdomain name', id: 'tunnel-subdomain'}) %>.fmtuner.org<br>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-250 br-15', password: true, placeholder: '', label: 'Token', id: 'tunnel-token'}) %>
|
||||
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Subdomain name', id: 'tunnel-subdomain'}) %>.fmtuner.org
|
||||
|
||||
<p>Enabling low latency mode may provide better experience, however it will also use more bandwidth.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Low latency mode', id: 'tunnel-lowLatencyMode'}) %><br>
|
||||
|
||||
Reference in New Issue
Block a user