1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +01:00

css UI fixes, new panel, code optimizaiton, security fixes

This commit is contained in:
Marek Farkaš
2025-02-16 13:26:35 +01:00
parent d40d7f5435
commit bb50aff7b4
21 changed files with 1279 additions and 1054 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "fm-dx-webserver",
"version": "1.3.4",
"version": "1.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fm-dx-webserver",
"version": "1.3.4",
"version": "1.3.5",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "1.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "fm-dx-webserver",
"version": "1.3.4",
"version": "1.3.5",
"description": "FM DX Webserver",
"main": "index.js",
"scripts": {

View File

@@ -417,7 +417,7 @@ function handleData(wss, receivedData, rdsWss) {
}
})
.catch((error) => {
logError("Error fetching Tx info:", error);
console.log("Error fetching Tx info:", error);
});
// Send the updated data to the client

View File

@@ -1,4 +1,5 @@
const http = require('http');
const https = require('https');
const net = require('net');
const crypto = require('crypto');
const dataHandler = require('./datahandler');
@@ -56,44 +57,86 @@ function authenticateWithXdrd(client, salt, password) {
client.write('x\n');
}
function handleConnect(clientIp, currentUsers, ws) {
http.get(`http://ip-api.com/json/${clientIp}`, (response) => {
let data = '';
const ipCache = new Map();
response.on('data', (chunk) => {
function handleConnect(clientIp, currentUsers, ws, callback) {
if (ipCache.has(clientIp)) {
// Use cached location info
processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback);
return;
}
http.get(`http://ip-api.com/json/${clientIp}`, (response) => {
let data = "";
response.on("data", (chunk) => {
data += chunk;
});
response.on('end', () => {
response.on("end", () => {
try {
const locationInfo = JSON.parse(data);
const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' };
const connectionTime = new Date().toLocaleString([], options);
if (locationInfo.as?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
return;
}
if(locationInfo.country === undefined) {
const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
} else {
const userLocation = `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
const userData = { ip: clientIp, location: userLocation, time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.country}`);
}
ipCache.set(clientIp, locationInfo); // Store in cache
processConnection(clientIp, locationInfo, currentUsers, ws, callback);
} catch (error) {
console.log(error);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
console.error("Error parsing location data:", error);
callback("User allowed");
}
});
}).on('error', (err) => {
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
}).on("error", (err) => {
console.error("Error fetching location data:", err);
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);
https.get("https://fmdx.org/banned_as.json", (banResponse) => {
let banData = "";
banResponse.on("data", (chunk) => {
banData += chunk;
});
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");
} catch (error) {
console.error("Error parsing banned AS list:", error);
callback("User allowed");
}
});
}).on("error", (err) => {
console.error("Error fetching banned AS list:", err);
callback("User allowed");
});
}
function formatUptime(uptimeInSeconds) {
const secondsInMinute = 60;
const secondsInHour = secondsInMinute * 60;

View File

@@ -326,174 +326,144 @@ app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../web'));
app.use('/', endpoints);
// Anti-spam function
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
const command = message.toString();
const now = Date.now();
if (endpointName === 'text') logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
// Initialize user command history if not present
if (!userCommandHistory[clientIp]) {
userCommandHistory[clientIp] = [];
}
// Record the current timestamp for the user
userCommandHistory[clientIp].push(now);
// Remove timestamps older than 20 ms from the history
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
// Check if there are 8 or more commands in the last 20 ms
if (userCommandHistory[clientIp].length >= 8) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
// Add to banlist if not already banned
if (!serverConfig.webserver.banlist.includes(clientIp)) {
serverConfig.webserver.banlist.push(clientIp);
logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
console.log(serverConfig.webserver.banlist);
configSave();
}
ws.close(1008, 'Bot-like behavior detected');
return command; // Return command value before closing connection
}
// Update the last message time for general spam detection
lastMessageTime = now;
// Initialize command history for rate-limiting checks
if (!userCommands[command]) {
userCommands[command] = [];
}
// Record the current timestamp for this command
userCommands[command].push(now);
// Remove timestamps older than 1 second
userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000);
// If command count exceeds limit, close connection
if (userCommands[command].length > lengthCommands) {
if (now - lastWarn.time > 1000) { // Check if 1 second has passed
logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`);
lastWarn.time = now; // Update the last warning time
}
ws.close(1008, 'Spamming detected');
return command; // Return command value before closing connection
}
return command; // Return command value for normal execution
}
/**
* WEBSOCKET BLOCK
*/
const tunerLockTracker = new WeakMap();
wss.on('connection', (ws, request) => {
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const userCommandHistory = {};
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
return;
}
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const userCommandHistory = {};
if (clientIp.includes(',')) {
clientIp = clientIp.split(',')[0].trim();
}
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);
if(currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n');
}
helpers.handleConnect(clientIp, currentUsers, ws);
// Anti-spam tracking for each client
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', (message) => {
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`);
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
return;
}
if (command.includes("\'")) {
return;
if (clientIp.includes(',')) {
clientIp = clientIp.split(',')[0].trim();
}
if (command.startsWith('w') && request.session.isAdminAuthenticated) {
switch (command) {
case 'wL1': serverConfig.lockToAdmin = true; break;
case 'wL0': serverConfig.lockToAdmin = false; break;
case 'wT0': serverConfig.publicTuner = true; break;
case 'wT1': serverConfig.publicTuner = false; break;
default: break;
}
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 (command.startsWith('T')) {
const tuneFreq = Number(command.slice(1)) / 1000;
const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) {
return;
}
}
helpers.handleConnect(clientIp, currentUsers, ws, (result) => {
if (result === "User banned") {
ws.close(1008, 'Banned IP');
return;
}
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
output.write(`${command}\n`);
}
});
ws.on('close', (code, reason) => {
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);
if (index !== -1) {
storage.connectedUsers.splice(index, 1);
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
}
if(currentUsers === 0) {
storage.connectedUsers = [];
}
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
setTimeout(function() {
if(currentUsers === 0) {
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n');
dataHandler.resetToDefault(dataHandler.dataToSend);
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
}
}, 10000)
}
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
client.write('X\n');
}
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
});
ws.on('error', console.error);
});
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', (message) => {
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`);
return;
}
if (command.includes("\'")) {
return;
}
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) {
switch (command) {
case 'wL1':
if (isAdminAuthenticated) serverConfig.lockToAdmin = true;
break;
case 'wL0':
if (isAdminAuthenticated) serverConfig.lockToAdmin = false;
break;
case 'wT0':
serverConfig.publicTuner = true;
if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
break;
case 'wT1':
serverConfig.publicTuner = false;
if(!isAdminAuthenticated) tunerLockTracker.set(ws, true);
break;
default:
break;
}
}
if (command.startsWith('T')) {
const tuneFreq = Number(command.slice(1)) / 1000;
const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) {
return;
}
}
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
output.write(`${command}\n`);
}
});
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--;
}
dataHandler.showOnlineUsers(currentUsers);
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
if (index !== -1) {
storage.connectedUsers.splice(index, 1);
}
if (currentUsers === 0) {
storage.connectedUsers = [];
}
if (tunerLockTracker.has(ws)) {
logInfo(`User who locked the tuner left. Unlocking the tuner.`);
output.write('wT0\n')
tunerLockTracker.delete(ws);
serverConfig.publicTuner = true;
}
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true &&
serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
setTimeout(function() {
if (currentUsers === 0) {
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
dataHandler.resetToDefault(dataHandler.dataToSend);
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
}
}, 10000);
}
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
client.write('X\n');
}
if (code !== 1008) {
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
}
});
ws.on('error', console.error);
});
// CHAT WEBSOCKET BLOCK
chatWss.on('connection', (ws, request) => {

View File

@@ -14,9 +14,16 @@ h1 {
}
h1#tuner-name {
font-size: 32px;
font-size: 26px;
font-weight: 300;
text-transform: initial;
user-select: none;
cursor: pointer;
transition: 0.3s ease color;
}
h1#tuner-name:hover {
color: var(--color-main-bright);
}
h2 {
@@ -43,7 +50,8 @@ h4 {
.tooltiptext {
position: absolute;
background-color: var(--color-3);
background-color: var(--color-2);
border: 2px solid var(--color-3);
color: var(--color-text);
text-align: center;
font-size: 14px;
@@ -54,7 +62,7 @@ h4 {
transition: opacity 0.3s ease;
}
p#tuner-desc {
p.tuner-desc {
margin: 0;
}
@@ -116,61 +124,63 @@ table .form-group {
margin: 0;
}
#settings, #back-btn, #users-online-container {
background: transparent;
border: 0;
color: var(--color-text);
.hidden-panel {
display: none;
position: absolute;
top: 15px;
right: 15px;
font-size: 16px;
width: 64px;
height: 64px;
line-height: 64px;
text-align: center;
border-radius: 50%;
transition: 500ms ease background;
cursor: pointer;
left: 0;
top: 100%;
width: 100%;
max-width: 1160px;
background: var(--color-1-transparent);
color: white;
text-align: left;
padding: 20px;
backdrop-filter: blur(5px);
z-index: 10;
border-radius: 0 0 15px 15px;
}
.chatbutton.hide-desktop {
#settings, #users-online-container, .chatbutton {
background: transparent;
border: 0;
color: var(--color-text);
position: absolute;
top: 15px;
left: 15px;
color: var(--color-4);
font-size: 16px;
width: 64px;
height: 64px;
line-height: 64px;
width: 48px;
height: 48px;
text-align: center;
border-radius: 50%;
transition: 500ms ease background;
border-radius: 15px;
transition: 300ms ease background;
cursor: pointer;
margin: 2px;
}
#settings:hover, #back-btn:hover, #users-online-container:hover {
background: var(--color-3);
#users-online-container {
margin-left: 10px;
}
.chatbutton, #settings {
background-color: var(--color-1);
}
#settings:hover, #users-online-container:hover, .chatbutton:hover {
background: var(--color-2);
}
#users-online-container {
top: 80px;
}
#back-btn {
left: 15px;
right: auto;
#af-list {
overflow-y: auto;
max-height: 345px;
}
#af-list ul {
display:list-item;
display: list-item;
padding: 0;
list-style-type: none;
margin-bottom: 0;
margin: 0;
font-size: 14px;
max-height: 380px;
overflow-y: auto;
}
#af-list a {
@@ -192,9 +202,9 @@ table .form-group {
margin-bottom: 0;
display: none;
cursor: pointer;
}
.checkbox label {
}
.checkbox label {
cursor: pointer;
display: block;
user-select: none;
@@ -204,45 +214,45 @@ table .form-group {
border: 2px solid var(--color-4);
box-sizing: border-box;
transition: 0.35s ease background-color, 0.35s ease color;
}
.checkbox label:hover {
}
.checkbox label:hover {
background-color: var(--color-2);
}
.form-group input:checked + label {
}
.form-group input:checked + label {
background-color: var(--color-4);
color: var(--color-main);
}
}
.tuner-info {
.tuner-info {
margin-top: 0px !important;
margin-bottom: 0px !important;
}
}
h2.settings-heading {
h2.settings-heading {
font-size: 42px;
padding: 10px 0;
font-weight: 300;
}
}
h3.settings-heading {
h3.settings-heading {
font-size: 24px;
text-transform: uppercase;
font-weight: 300;
margin-bottom: 5px;
}
}
#tuner-wireless {
#tuner-wireless {
display: none;
}
}
#flags-container-phone,
#flags-container-desktop {
#flags-container-phone,
#flags-container-desktop {
position: relative; /* Confine overlay within container which is necessary for iPhones */
}
}
#flags-container-phone .overlay,
#flags-container-desktop .overlay {
#flags-container-phone .overlay,
#flags-container-desktop .overlay {
position: absolute;
top: 0;
left: 0;
@@ -321,23 +331,28 @@ pre {
position: relative;
}
.text-200-px {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 200px;
}
@media (max-width: 768px) {
canvas, #flags-container {
display: none;
}
#tuner-desc {
.tuner-desc {
margin-bottom: 20px !important;
text-align: center;
}
#ps-container {
background-color: var(--color-1-transparent);
height: 100px !important;
margin: auto !important;
margin-top: 30px !important;
width: 100%;
}
h1#tuner-name {
max-width: 90%;
margin: auto;
}
h2 {
display: none;
}
@@ -364,7 +379,7 @@ pre {
font-size: 42px;
}
#data-frequency {
font-size: 64px;
font-size: 58px;
}
#data-rt0, #data-rt1 {
font-size: 10px;
@@ -426,7 +441,7 @@ pre {
}
}
@media only screen and (min-width: 769px) and (max-height: 860px) {
@media only screen and (min-width: 769px) and (max-height: 720px) {
#rt-container {
height: 90px !important;
}
@@ -441,38 +456,33 @@ pre {
font-size: 24px;
text-align: left;
}
.tuner-info #tuner-limit {
float: left;
text-align: left;
}
.tuner-info #tuner-desc {
float: right;
text-align: right;
}
h2 {
margin-bottom: 10px;
font-size: 18px;
}
h2.signal-heading {
margin-bottom: 0;
}
.highest-signal-container {
margin-bottom: -10px !important;
}
h2.mb-0 {
margin-bottom: 0;
margin-top: 2px !important;
}
#af-list ul {
height: 225px !important;
}
.chatbutton {
height: 88px !important;
#af-list {
overflow-y: auto;
max-height: 330px;
}
}

View File

@@ -71,6 +71,11 @@
margin: 0 !important;
}
.m-10 {
margin: 10px;
}
.m-left-20 {
margin-left: 20px;
}
@@ -186,6 +191,10 @@
margin-top: 25px;
}
.bottom-10 {
margin-bottom: 10px;
}
.bottom-20 {
margin-bottom: 20px;
}
@@ -238,12 +247,34 @@ table .input-text {
animation: blinker 1.5s infinite;
}
.scrollable-container {
display: flex;
gap: 8px;
overflow-x: auto; /* Enables horizontal scrolling */
white-space: nowrap;
scrollbar-width: none; /* Hide scrollbar in Firefox */
-ms-overflow-style: none; /* Hide scrollbar in Edge */
}
/* Hide scrollbar for Chrome, Safari */
.scrollable-container::-webkit-scrollbar {
display: none;
}
/* Chevron styling */
.scroll-left,
.scroll-right {
display: none; /* Hidden by default */
cursor: pointer;
width: 48px;
}
@keyframes blinker {
0% {
background-color: var(--color-4);
background-color: var(--color-3);
}
100% {
background-color: var(--color-2);
background-color: var(--color-1);
}
}
@@ -286,7 +317,7 @@ table .input-text {
}
/* Laptop compact view */
@media only screen and (min-width: 960px) and (max-height: 860px) {
@media only screen and (min-width: 960px) and (max-height: 720px) {
.text-big {
font-size: 40px;
}

View File

@@ -60,14 +60,22 @@ body {
min-height: 100%;
}
#wrapper-outer {
.wrapper-outer {
width: 100%;
height: auto;
background-color: var(--color-main);
display: flex;
align-items: center;
justify-content: center;
min-height:100vh;
flex-direction: column;
}
.wrapper-outer:not(.dashboard-panel) {
min-height: calc(100vh - 84px);
}
.wrapper-outer.wrapper-full {
min-height: 100vh;
}
.wrapper-outer-static {
@@ -79,6 +87,7 @@ body {
width: 100%;
max-width: calc(0% + 1180px);
}
#wrapper.setup-wrapper {
margin: auto;
position: static;
@@ -135,15 +144,6 @@ hr {
border-radius: 0px 15px 15px 0px;
}
@media (max-width: 1180px) {
#wrapper {
position: static;
transform: none;
margin: 50px auto;
width: 100%;
}
}
@media (max-width: 768px) {
#wrapper.setup-wrapper {
width: 100%;

View File

@@ -107,11 +107,6 @@ body.modal-open {
overflow-y: auto;
}
.modal-panel-content .version-info {
margin-top: 20px;
width: 100%;
}
.modal-panel-footer {
width: 450px;
height: 100px;
@@ -171,7 +166,7 @@ body.modal-open {
width: 100%;
}
.modal-panel-chat {
height: 550px;
height: 510px;
}
#chat-chatbox {
height: 333px !important;

View File

@@ -93,21 +93,13 @@
.panel-90 {
margin-top: 100px;
}
}
/* Laptop compact view */
@media only screen and (min-width: 960px) and (max-height: 860px) {
*[class^="panel-"] {
margin-top: 20px;
.panel-100-real.bg-phone {
background-color: var(--color-1-transparent);
backdrop-filteR: blur(5px) !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.panel-90 {
margin-top: 0;
}
.panel-10 {
padding-bottom: 20px;
padding-right: 20px;
}
.panel-10.hide-phone {
padding: 0;
#dashboard-panel-description {
backdrop-filter: blur(25px) !important;
}
}

View File

@@ -2,7 +2,7 @@
#toast-container {
position: fixed;
top: 20px;
right: 96px;
right: 32px;
z-index: 9999;
}

View File

@@ -6,10 +6,10 @@
<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 rel="icon" type="image/png" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="FM-DX WebServer [<%= tunerName %>]">
<meta property="og:type" content="website">
<meta property="og:image" content="https://fmdx.org/img/webserver_icon.png">
@@ -29,471 +29,521 @@
<script src="js/3las/3las.js"></script>
<script src="js/3las/main.js"></script>
<script type="text/javascript">
window.addEventListener('load', Init, false);
document.ontouchmove = function(e){
e.preventDefault();
}
window.addEventListener('load', Init, false);
document.ontouchmove = function(e){
e.preventDefault();
}
</script>
</head>
<body>
<div id="wrapper-outer">
<div id="wrapper">
<div class="panel-100 no-bg tuner-info">
<h1 id="tuner-name"><span class="text-bold" style="color: var(--color-text);">[</span> <%= tunerName %> <span class="text-bold" style="color: var(--color-text);">]</span>
<% if (!publicTuner) { %><i class="fa-solid fa-key pointer tooltip" aria-label="Only people with tune password can tune." data-tooltip="Only people with tune password can tune."></i>
<% } if (tunerLock) { %><i class="fa-solid fa-lock pointer tooltip" aria-label="Tuner is currently locked to admin." data-tooltip="Tuner is currently locked to admin."></i>
<% } %>
<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>
</h1>
<p id="tuner-desc">
<%- tunerDesc %>
<% if(tuningLimit && tuningLimit == true){ %>
<br><span class="text-small">Limit: <span class="color-4"><%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz</span></span><br>
<% } %>
</p>
<div style="clear: both"></div>
</div>
<div class="canvas-container hide-phone">
<canvas id="signal-canvas"></canvas>
</div>
<div class="flex-container">
<div class="panel-100 no-bg" style="margin-top: 0; margin-left: 0;">
<div class="flex-container">
<div class="panel-75 flex-container no-bg">
<div class="panel-10 no-bg h-100 m-0 m-right-20 hide-phone" style="width: 100px;margin-right: 20px !important;">
<button class="playbutton" aria-label="Play / Stop"><i class="fa-solid fa-play fa-lg"></i></button>
</div>
<div class="panel-100 m-0 hover-brighten flex-center tooltip" id="ps-container" style="height: 90px;" data-tooltip="Clicking on the RDS PS will copy the RDS info into the clipboard.">
<span class="text-big" id="data-ps"></span>
</div>
</div>
<div id="flags-container-desktop" class="panel-33 user-select-none">
<h2 class="show-phone">
<div class="data-pty color-4"></div>
</h2>
<h3 style="margin-top:0;margin-bottom:0;" class="text-color-default flex-center">
<span class="data-tp">TP</span>
<span style="margin-left: 15px;" class="data-ta">TA</span>
<div style="display:inline-block">
<span style="margin-left: 20px;display: block;margin-top: 2px;" class="data-flag"></span>
</div>
<span class="pointer stereo-container" style="position: relative; margin-left: 20px;" role="button" aria-label="Stereo / Mono toggle" tabindex="0">
<div class="circle-container">
<div class="circle data-st circle1"></div>
<div class="circle data-st circle2"></div>
</div>
<span class="overlay tooltip" data-tooltip="Stereo / Mono toggle. <br><strong>Click to toggle."></span>
</span>
<span style="margin-left: 15px;" class="data-ms">MS</span>
</h3>
</div>
</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.">
<h2 class="signal-heading">PI CODE</h2>
<div class="text-small text-gray highest-signal-container">
<span id="data-regular-pi">&nbsp;</span>
</div>
<span id="data-pi" class="text-big text-uppercase"></span>
</div>
<div class="panel-33 hover-brighten" id="freq-container">
<h2>FREQUENCY</h2>
<span id="data-frequency" class="text-big"></span>
</div>
<div class="panel-33">
<h2 class="signal-heading">SIGNAL</h2>
<div class="text-small text-gray highest-signal-container">
<i class="fa-solid fa-arrow-up"></i>
<span id="data-signal-highest"></span>
<% if (device == 'sdr') { %> <span>dB SNR</span> <% } else { %> <span class="signal-units"></span> <% } %>
</div>
<div class="text-big">
<span id="data-signal"></span><!--
--><span id="data-signal-decimal" class="text-medium-big" style="opacity:0.7;"></span>
<% if (device == 'sdr') { %> <span class="text-medium">dB SNR</span> <% } else { %> <span class="signal-units text-medium">dBf</span> <% } %>
</div>
</div>
</div>
<div class="flex-container flex-phone flex-phone-column">
<div class="panel-33 no-bg filter-controls" style="height: 48px;">
<div class="flex-container no-filter flex-phone h-100">
<div class="panel-75 no-bg h-100 m-0 hide-desktop m-right-20 button-play-mobile" style="margin-right: 20px;">
<button class="playbutton" aria-label="Play/Stop"><i class="fa-solid fa-play"></i></button>
</div>
<% if (antennas.enabled == true) { %>
<div class="panel-50 no-bg h-100 br-0 m-0 dropdown dropdown-up" id="data-ant" style="margin-right: 25px;">
<input type="text" placeholder="Ant A" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<% if(antennas.ant1.enabled == true) { %><li data-value="0" class="option" tabindex="0"><%= antennas.ant1.name %></li><% } %>
<% if(antennas.ant2.enabled == true) { %><li data-value="1" class="option" tabindex="0"><%= antennas.ant2.name %></li><% } %>
<% if(antennas.ant3.enabled == true) { %><li data-value="2" class="option" tabindex="0"><%= antennas.ant3.name %></li><% } %>
<% if(antennas.ant4.enabled == true) { %><li data-value="3" class="option" tabindex="0"><%= antennas.ant4.name %></li><% } %>
</ul>
</div>
<% } %>
<div class="panel-50 no-bg br-0 h-100 m-0 button-eq">
<% if (device == 'tef') { %><button id="data-eq" style="border-radius: 15px 0px 0px 15px;" class="tooltip" aria-label="EQ Filter" data-tooltip="<strong>The cEQ filter can reduce bandwidth below 56 kHz.</strong><br><br>Useful for weak stations next to strong ones,<br>although it may pick up more interference."><span class="text-bold">cEQ</span></button><% } %>
<% if (device == 'xdr') { %><button id="data-eq" style="border-radius: 15px 0px 0px 15px;" class="tooltip" aria-label="RF+ Filter" data-tooltip="<strong>The RF+ filter increases gain by 5dB</strong>"><span class="text-bold">RF+</span></button><% } %>
</div>
<div class="panel-50 no-bg br-0 h-100 m-0 button-ims">
<% if (device == 'tef') { %><button id="data-ims" style="border-radius: 0px 15px 15px 0px;" class="tooltip" aria-label="iMS + Filter" data-tooltip="<strong>The iMS filter reduces multipath audio artifacts.</strong><br><br>It's recommended to leave it on most of the time."><span class="text-bold">iMS</span></button><% } %>
<% if (device == 'xdr') { %><button id="data-ims" style="border-radius: 0px 15px 15px 0px;" class="tooltip" aria-label="IF+ Filter" data-tooltip="<strong>The IF+ filter increases gain by 6dB</strong>"><span class="text-bold">IF+</span></button><% } %>
</div>
</div>
</div>
<div class="panel-33 flex-container flex-phone no-bg" id="tune-buttons">
<button id="freq-down" aria-label="Tune down"><i class="fa-solid fa-chevron-left"></i></button>
<input type="text" id="commandinput" inputmode="numeric" placeholder="Frequency" autocomplete="off" aria-label="Current frequency: ">
<button id="freq-up" aria-label="Tune up"><i class="fa-solid fa-chevron-right"></i></button>
</div>
<div class="panel-33 hide-phone no-bg">
<div class="flex-container">
<span class="panel-100-real m-0" style="height: 48px;">
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1" aria-label="Volume slider">
</span>
<% if (device == 'tef' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" class="option" tabindex="0">Auto</li>
<li data-value="56000" class="option" tabindex="0">56 kHz</li>
<li data-value="64000" class="option" tabindex="0">64 kHz</li>
<li data-value="72000" class="option" tabindex="0">72 kHz</li>
<li data-value="84000" class="option" tabindex="0">84 kHz</li>
<li data-value="97000" class="option" tabindex="0">97 kHz</li>
<li data-value="114000" class="option" tabindex="0">114 kHz</li>
<li data-value="133000" class="option" tabindex="0">133 kHz</li>
<li data-value="151000" class="option" tabindex="0">151 kHz</li>
<li data-value="184000" class="option" tabindex="0">184 kHz</li>
<li data-value="200000" class="option" tabindex="0">200 kHz</li>
<li data-value="217000" class="option" tabindex="0">217 kHz</li>
<li data-value="236000" class="option" tabindex="0">236 kHz</li>
<li data-value="254000" class="option" tabindex="0">254 kHz</li>
<li data-value="287000" class="option" tabindex="0">287 kHz</li>
<li data-value="311000" class="option" tabindex="0">311 kHz</li>
</ul>
</div>
<% } %>
<% if (device == 'xdr' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" data-value2="-1" class="option" tabindex="0">Auto</li>
<li data-value="55000" data-value2="0" class="option" tabindex="0">55 kHz</li>
<li data-value="73000" data-value2="1" class="option" tabindex="0">73 kHz</li>
<li data-value="90000" data-value2="2" class="option" tabindex="0">90 kHz</li>
<li data-value="108000" data-value2="3" class="option" tabindex="0">108 kHz</li>
<li data-value="125000" data-value2="4" class="option" tabindex="0">125 kHz</li>
<li data-value="142000" data-value2="5" class="option" tabindex="0">142 kHz</li>
<li data-value="159000" data-value2="6" class="option" tabindex="0">159 kHz</li>
<li data-value="177000" data-value2="7" class="option" tabindex="0">177 kHz</li>
<li data-value="194000" data-value2="8" class="option" tabindex="0">194 kHz</li>
<li data-value="211000" data-value2="9" class="option" tabindex="0">211 kHz</li>
<li data-value="229000" data-value2="10" class="option" tabindex="0">229 kHz</li>
<li data-value="246000" data-value2="11" class="option" tabindex="0">246 kHz</li>
<li data-value="263000" data-value2="12" class="option" tabindex="0">263 kHz</li>
<li data-value="281000" data-value2="13" class="option" tabindex="0">281 kHz</li>
<li data-value="298000" data-value2="14" class="option" tabindex="0">298 kHz</li>
<li data-value="309000" data-value2="15" class="option" tabindex="0">309 kHz</li>
</ul>
</div>
<% } %>
<% if (device == 'sdr' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" class="option" tabindex="0">Auto</li>
<li data-value="4000" class="option" tabindex="0">4 kHz</li>
<li data-value="8000" class="option" tabindex="0">8 kHz</li>
<li data-value="10000" class="option" tabindex="0">10 kHz</li>
<li data-value="20000" class="option" tabindex="0">20 kHz</li>
<li data-value="30000" class="option" tabindex="0">30 kHz</li>
<li data-value="50000" class="option" tabindex="0">50 kHz</li>
<li data-value="75000" class="option" tabindex="0">75 kHz</li>
<li data-value="100000" class="option" tabindex="0">100 kHz</li>
<li data-value="125000" class="option" tabindex="0">125 kHz</li>
<li data-value="150000" class="option" tabindex="0">150 kHz</li>
<li data-value="175000" class="option" tabindex="0">175 kHz</li>
<li data-value="200000" class="option" tabindex="0">200 kHz</li>
<li data-value="225000" class="option" tabindex="0">225 kHz</li>
</ul>
</div>
<% } %>
<% if (fmlist_integration == true) { %>
<button class="tooltip bg-color-4 popup" id="log-fmlist"
data-tooltip="<strong>LOG TO FMLIST</strong><br>Clicking this button logs the current station to FMLIST's visual logbook." aria-label="Log to FMLIST"
style="width: 80px; height: 48px;margin-left: 15px !important;">
<i class="fa-solid fa-flag fa-lg"></i>
<span class="popup-content">
Choose the DX propagation type:<br>
<a class="top-10 bg-color-3 text-bold" style="padding: 10px; border-radius: 15px 0 0 15px; display: inline-block;" id="log-fmlist-tropo">Tropo</a><!--
--><a class="top-10 bg-color-3 text-bold" style="padding: 10px; border-radius: 0 15px 15px 0; display: inline-block;" id="log-fmlist-sporadice">Sporadic-E</a>
</span>
</button>
<% } %>
</div>
</div>
</div>
<div class="flex-container flex-phone flex-phone-column">
<div class="panel-75 hover-brighten" id="rt-container" style="height: 100px;">
<h2 style="margin-top: 4px;">RADIOTEXT</h2>
<div id="data-rt0">
<span></span>
</div>
<div id="data-rt1">
<span></span>
</div>
<hr class="hide-desktop">
</div>
<div class="panel-33 hover-brighten tooltip" 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">
<span id="data-station-name"></span>
</h2>
<h4 class="m-0">
<span id="data-station-city" style="font-size: 16px;"></span> <span class="text-small">[<span id="data-station-itu"></span>]</span>
</h4>
<span class="text-small">
<span id="data-station-erp"></span> kW [<span id="data-station-pol"></span>] <span class="text-gray">•</span> <span id="data-station-distance"></span> <span class="text-gray">•</span> <span id="data-station-azimuth"></span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-10 no-bg m-0">
<div class="panel-100 w-100" style="margin-left: 0;">
<h2>AF</h2>
<div id="af-list" class="p-bottom-20" style="text-align: center;">
<% if (chatEnabled) { %><ul style="height: 231px;">
<% } else { %>
<ul style="height: 351px;">
<% } %>
</ul>
</div>
</div>
<% if (chatEnabled) { %>
<div class="panel-10 no-bg h-100 hide-phone" style="width: 100px; height: 100px; margin-left: 0;">
<button class="chatbutton bg-color-2" aria-label="Chatbox"><i class="fa-solid fa-comments fa-lg"></i> (<span class="chat-messages-count">0</span>)</button>
</div>
<% if(!publicTuner || tunerLock) { %>
<div class="tuner-status p-10 color-3">
<% if (!publicTuner) { %><i class="fa-solid fa-key pointer tooltip fa-lg" aria-label="Only people with tune password can tune." data-tooltip="Only people with tune password can tune."></i>
<% } if (tunerLock) { %><i class="fa-solid fa-lock pointer tooltip fa-lg" aria-label="Tuner is currently locked to admin." data-tooltip="Tuner is currently locked to admin."></i>
<% } %>
</div>
</div>
<div id="flags-container-phone" class="panel-33">
<h2 class="show-phone">
<div class="data-pty text-color-default"></div>
</h2>
<h3 style="margin-top:0;margin-bottom:0;" class="colornode-4 flex-center">
<span class="data-tp">TP</span>
<span style="margin-left: 15px;" class="data-ta">TA</span>
<div style="display:inline-block">
<span style="margin-left: 20px;display: block;margin-top: 2px;" class="data-flag"></span>
</div>
<span class="pointer stereo-container" style="position: noderelative; margin-left: 20px;" role="button" aria-label="Stereo / Mono toggle" tabindex="0">
<div class="circle-container">
<div class="circle data-st circle1"></div>
<div class="circle data-st circle2"></div>
</div>
<span class="overlay tooltip" data-tooltip="Stereo / Mono toggle. <br><strong>Click to toggle."></span>
</span>
<span style="margin-left: 15px;" class="data-ms">MS</span>
</h3>
</div>
</div>
<button id="settings" aria-label="Settings"><i class="fa-solid fa-gear"></i></button>
<% if (chatEnabled) { %>
<button class="chatbutton hide-desktop bg-color-2" aria-label="Chatbox"><i class="fa-solid fa-comments fa-lg"></i> (<span class="chat-messages-count">0</span>)</button>
<% } %>
<button id="users-online-container" class="hide-phone" aria-label="Online users"><i class="fa-solid fa-user"></i> <span class="users-online"></span></button>
<div id="myModal" class="modal">
<div class="modal-panel">
<div class="flex-container flex-phone" style="height: calc(100% - 100px)">
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close settings" tabindex="0"><i class="fa-solid fa-chevron-right"></i></div>
<div class="modal-panel-content">
<h1 class="top-25">Settings</h1>
<div class="panel-full flex-center no-bg m-0">
<%- include('_components', { component: 'dropdown', id: 'theme-selector', inputId: 'theme-selector-input', label: 'Theme', cssClass: '', placeholder: 'Default',
options: [
{ value: 'theme1', label: 'Mint' },
{ value: 'theme2', label: 'Cappuccino' },
{ value: 'theme3', label: 'Nature' },
{ value: 'theme4', label: 'Ocean' },
{ value: 'theme5', label: 'Terminal' },
{ value: 'theme6', label: 'Nightlife' },
{ value: 'theme7', label: 'Blurple' },
{ value: 'theme8', label: 'Construction' },
{ value: 'theme9', label: 'Amoled' },
]
}) %>
</div>
<% if (device !== 'sdr') { %>
<div class="panel-full flex-center no-bg m-0">
<%- include('_components', { component: 'dropdown', id: 'signal-selector', inputId: 'signal-selector-input', label: 'Signal units', cssClass: '', placeholder: 'dBf',
options: [
{ value: 'dbf', label: 'dBf' },
{ value: 'dbuv', label: 'dBuV' },
{ value: 'dbm', label: 'dBm' },
]
}) %>
</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="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 class="flex-container flex-phone" style="flex: 1;overflow-x: auto;align-items: center;">
<div class="plugin-list scroll-left">
<i class="fa-solid fa-chevron-left fa-lg color-4"></i>
</div>
<div class="dashboard-panel-plugin-list flex-container hide-phone" style="flex: 1;overflow-x: auto;">
<div class="flex-container scrollable-container"></div>
</div>
<div class="plugin-list scroll-right">
<i class="fa-solid fa-chevron-right fa-lg color-4"></i>
</div>
</div>
<% if (isAdminAuthenticated) { %>
<p class="color-3">You are logged in as an adminstrator.</p>
<div class="admin-quick-dashboard">
<div class="icon tooltip <% if (tunerLock) { %>active<% } %>" id="dashboard-lock-admin" onClick="toggleLock('#dashboard-lock-admin', 'wL1', 'wL0', 'Unlock Tuner (Admin)', 'Lock Tuner (Admin)');" role="button" aria-label="Toggle admin lock until restart" tabindex="0" data-tooltip="Toggle admin lock<br>Lasts until restart">
<i class="fa-solid fa-lock"></i>
</div>
<div class="icon tooltip <% if (!publicTuner) { %>active<% } %>" id="dashboard-lock-tune" onClick="toggleLock('#dashboard-lock-tune', 'wT1', 'wT0', 'Unlock Tuner (Password tune)', 'Lock Tuner (Password tune)');" role="button" aria-label="Toggle password lock until restart" tabindex="0" data-tooltip="Toggle password lock<br>Lasts until restart">
<i class="fa-solid fa-key"></i>
</div>
<div class="icon tooltip" role="button" aria-label="Go to admin panel" tabindex="0" data-tooltip="Go to admin panel" onClick="window.open('./setup', '_blank').focus();">
<i class="fa-solid fa-user"></i>
</div>
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
<i class="fa-solid fa-sign-out"></i>
</div>
</div>
<% } else if (isTuneAuthenticated) { %>
<p class="color-3">You are logged in and can control the receiver.</p>
<div class="admin-quick-dashboard">
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
<i class="fa-solid fa-sign-out"></i>
</div>
</div>
<% } else { %>
<form action="./login" method="post" id="login-form" class="top-25">
<input type="password" id="password" name="password" placeholder="Password" style="width: 145px; border-radius: 15px 0 0 15px" required>
<button type="submit" class="br-0 top-10 tooltip" style="height: 46px; width: 50px; margin-left: -2px;border-radius: 0 15px 15px 0;" role="button" aria-label="Log in" tabindex="0" data-tooltip="Log in">
<i class="fa-solid fa-right-to-bracket"></i>
</button>
</form>
<div style="margin-left: auto;" class="dashboard-panel-plugin-content"></div>
<div>
<button id="users-online-container" class="hide-phone" aria-label="Online users"><i class="fa-solid fa-user"></i> <span class="users-online"></span></button>
<% if (chatEnabled) { %>
<button class="chatbutton hide-phone" aria-label="Chatbox"><i class="fa-solid fa-comments"></i></button>
<% } %>
<button id="settings" aria-label="Settings" class="hide-phone"><i class="fa-solid fa-gear"></i></button>
</div>
<div id="dashboard-panel-description" class="hidden-panel">
<div class="flex-container">
<div class="tuner-desc">
<%- tunerDesc %>
<% if(tuningLimit && tuningLimit == true){ %>
<br><span class="text-small">Limit: <span class="color-4"><%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz</span></span><br>
<% } %>
<div id="login-message" class="color-3">&nbsp;</div>
<hr class="color-2 auto">
</div>
<table class="auto text-left p-10">
<tr>
<td>Device:</td>
<td class="color-3 p-left-10">
<div class="flex-phone" style="margin-left: auto;flex-direction: row-reverse;justify-content: space-around;">
<div class="flex-container flex-phone" style="text-align: right;justify-content: right;align-items: center; height: 64px;">
<div>
<span class="">Device</span><br>
<span class="text-small color-4">
<% if (device == 'tef') { %>TEF668x<% } %>
<% if (device == 'xdr') { %>Sony XDR<% } %>
<% if (device == 'sdr') { %>SDR<% } %>
</td>
</tr>
<tr>
<td>Server Time:</td>
<td class="color-3 p-left-10" id="server-time"></td>
</tr>
<tr>
<td>Local Time:</td>
<td class="color-3 p-left-10" id="client-time"></td>
</tr>
</table>
<table class="auto">
<tr>
<td class="p-left-10">P1: <a class="color-3 pointer" id="preset1">87.5</a></td>
<td class="p-left-10">P2: <a class="color-3 pointer" id="preset2"></a></td>
<td class="p-left-10">P3: <a class="color-3 pointer" id="preset3"></a></td>
<td class="p-left-10">P4: <a class="color-3 pointer" id="preset4"></a></td>
</tr>
</table>
<div class="version-info">
<p class="m-0">
FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span>
</p>
<p class="text-small m-0 color-3">by <a href="https://fmdx.org" target="_blank">FMDX.org</a></p>
<span class="text-small" style="color: var(--color-3);">[<a href="https://servers.fmdx.org/" target="_blank">Receiver Map</a>]</span>
<br>
<br>
<% if(ownerContact){ %>
<span>Owner contact:</span><br>
<span class="text-small m-0 bottom-20"><%= ownerContact %></span>
<% } %>
<p class="text-small color-3" id="current-ping"></p>
</span>
</div>
<div class="color-3 m-10 text-medium">
<i class="fa-solid fa-fw fa-radio"></i>
</div>
</div>
</div>
</div>
<div class="modal-panel-footer flex-container flex-phone">
<div class="modal-panel-sidebar" style="font-size: 22px;">
<div class="flex-center" style="height: 50px">
<i class="fa-solid fa-hand-holding-medical"></i>
</div>
<div class="flex-center" style="height: 50px">
<i class="fa-brands fa-discord"></i>
</div>
</div>
<div class="modal-panel-content">
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://buymeacoffee.com/noobish')">
<strong>Support</strong> the developer!
</div>
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://discord.com/invite/ZAVNdS74mC')">
Join our <strong>FMDX.org Discord</strong> community!
</div>
</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;">
<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 top-10" style="height: 48px; border-radius: 0 15px 15px 0;margin-left:-3px;" id="chat-nickname-save">Save</button>
<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 class="flex-container flex-phone" style="text-align: right;justify-content: right;align-items: center; height: 64px;">
<div>
<span class="">Server time</span><br>
<span class="text-small color-4" id="server-time"></span>
</div>
<div class="color-3 m-10 text-medium">
<i class="fa-solid fa-fw fa-stopwatch"></i>
</div>
</div>
</div>
<div style="width: 1px;background: var(--color-2);" class="m-10 hide-phone"></div>
<div>
<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>
<div class="flex-container flex-phone" style="align-items: center; height: 64px;">
<div class="color-3 m-10 text-medium">
<i class="fa-solid fa-fw fa-user-tie"></i>
</div>
<div>
<span class="">Owner contact</span><br>
<span class="text-small color-4 text-200-px tooltip" data-tooltip="<%= ownerContact %>" data-tooltip-placement="bottom"><%= ownerContact %></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="toast-container"></div>
<script src="js/websocket.js"></script>
<script src="js/webserver.js"></script>
<% if (!noPlugins) { %>
<% plugins?.forEach(function(plugin) { %>
<script src="js/plugins/<%= plugin %>"></script>
<% }); %>
<% } %>
</body>
</html>
<div class="wrapper-outer" style="z-index: 100;">
<div id="wrapper">
<div class="canvas-container hide-phone">
<canvas id="signal-canvas"></canvas>
</div>
<div class="flex-container">
<div class="panel-100 no-bg" style="margin-top: 0; margin-left: 0;">
<div class="flex-container">
<div class="panel-75 flex-container no-bg">
<div class="panel-10 no-bg h-100 m-0 m-right-20 hide-phone" style="width: 100px;margin-right: 20px !important;">
<button class="playbutton" aria-label="Play / Stop"><i class="fa-solid fa-play fa-lg"></i></button>
</div>
<div class="panel-100 m-0 hover-brighten flex-center tooltip" id="ps-container" style="height: 90px;" data-tooltip="Clicking on the RDS PS will copy the RDS info into the clipboard.">
<span class="text-big" id="data-ps"></span>
</div>
</div>
<div id="flags-container-desktop" class="panel-33 user-select-none">
<h2 class="show-phone">
<div class="data-pty color-4"></div>
</h2>
<h3 style="margin-top:0;margin-bottom:0;" class="text-color-default flex-center">
<span class="data-tp">TP</span>
<span style="margin-left: 15px;" class="data-ta">TA</span>
<div style="display:inline-block">
<span style="margin-left: 20px;display: block;margin-top: 2px;" class="data-flag"></span>
</div>
<span class="pointer stereo-container" style="position: relative; margin-left: 20px;" role="button" aria-label="Stereo / Mono toggle" tabindex="0">
<div class="circle-container">
<div class="circle data-st circle1"></div>
<div class="circle data-st circle2"></div>
</div>
<span class="overlay tooltip" data-tooltip="Stereo / Mono toggle. <br><strong>Click to toggle."></span>
</span>
<span style="margin-left: 15px;" class="data-ms">MS</span>
</h3>
</div>
</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.">
<h2 class="signal-heading">PI CODE</h2>
<div class="text-small text-gray highest-signal-container">
<span id="data-regular-pi">&nbsp;</span>
</div>
<span id="data-pi" class="text-big text-uppercase"></span>
</div>
<div class="panel-33 hover-brighten" id="freq-container">
<h2>FREQUENCY</h2>
<span id="data-frequency" class="text-big"></span>
</div>
<div class="panel-33">
<h2 class="signal-heading">SIGNAL</h2>
<div class="text-small text-gray highest-signal-container">
<i class="fa-solid fa-arrow-up"></i>
<span id="data-signal-highest"></span>
<% if (device == 'sdr') { %> <span>dB SNR</span> <% } else { %> <span class="signal-units"></span> <% } %>
</div>
<div class="text-big">
<span id="data-signal"></span><!--
--><span id="data-signal-decimal" class="text-medium-big" style="opacity:0.7;"></span>
<% if (device == 'sdr') { %> <span class="text-medium">dB SNR</span> <% } else { %> <span class="signal-units text-medium">dBf</span> <% } %>
</div>
</div>
</div>
<div class="flex-container flex-phone flex-phone-column">
<div class="panel-33 no-bg filter-controls" style="height: 48px;">
<div class="flex-container no-filter flex-phone h-100">
<div class="panel-75 no-bg h-100 m-0 hide-desktop m-right-20 button-play-mobile" style="margin-right: 20px;">
<button class="playbutton" aria-label="Play/Stop"><i class="fa-solid fa-play"></i></button>
</div>
<% if (antennas.enabled == true) { %>
<div class="panel-50 no-bg h-100 br-0 m-0 dropdown dropdown-up" id="data-ant" style="margin-right: 25px;">
<input type="text" placeholder="Ant A" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<% if(antennas.ant1.enabled == true) { %><li data-value="0" class="option" tabindex="0"><%= antennas.ant1.name %></li><% } %>
<% if(antennas.ant2.enabled == true) { %><li data-value="1" class="option" tabindex="0"><%= antennas.ant2.name %></li><% } %>
<% if(antennas.ant3.enabled == true) { %><li data-value="2" class="option" tabindex="0"><%= antennas.ant3.name %></li><% } %>
<% if(antennas.ant4.enabled == true) { %><li data-value="3" class="option" tabindex="0"><%= antennas.ant4.name %></li><% } %>
</ul>
</div>
<% } %>
<div class="panel-50 no-bg br-0 h-100 m-0 button-eq">
<% if (device == 'tef') { %><button id="data-eq" style="border-radius: 15px 0px 0px 15px;" class="tooltip" aria-label="EQ Filter" data-tooltip="<strong>The cEQ filter can reduce bandwidth below 56 kHz.</strong><br><br>Useful for weak stations next to strong ones,<br>although it may pick up more interference."><span class="text-bold">cEQ</span></button><% } %>
<% if (device == 'xdr') { %><button id="data-eq" style="border-radius: 15px 0px 0px 15px;" class="tooltip" aria-label="RF+ Filter" data-tooltip="<strong>The RF+ filter increases gain by 5dB</strong>"><span class="text-bold">RF+</span></button><% } %>
</div>
<div class="panel-50 no-bg br-0 h-100 m-0 button-ims">
<% if (device == 'tef') { %><button id="data-ims" style="border-radius: 0px 15px 15px 0px;" class="tooltip" aria-label="iMS + Filter" data-tooltip="<strong>The iMS filter reduces multipath audio artifacts.</strong><br><br>It's recommended to leave it on most of the time."><span class="text-bold">iMS</span></button><% } %>
<% if (device == 'xdr') { %><button id="data-ims" style="border-radius: 0px 15px 15px 0px;" class="tooltip" aria-label="IF+ Filter" data-tooltip="<strong>The IF+ filter increases gain by 6dB</strong>"><span class="text-bold">IF+</span></button><% } %>
</div>
</div>
</div>
<div class="panel-33 flex-container flex-phone no-bg" id="tune-buttons">
<button id="freq-down" aria-label="Tune down"><i class="fa-solid fa-chevron-left"></i></button>
<input type="text" id="commandinput" inputmode="numeric" placeholder="Frequency" autocomplete="off" aria-label="Current frequency: ">
<button id="freq-up" aria-label="Tune up"><i class="fa-solid fa-chevron-right"></i></button>
</div>
<div class="panel-33 hide-phone no-bg">
<div class="flex-container">
<span class="panel-100-real m-0" style="height: 48px;">
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1" aria-label="Volume slider">
</span>
<% if (device == 'tef' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" class="option" tabindex="0">Auto</li>
<li data-value="56000" class="option" tabindex="0">56 kHz</li>
<li data-value="64000" class="option" tabindex="0">64 kHz</li>
<li data-value="72000" class="option" tabindex="0">72 kHz</li>
<li data-value="84000" class="option" tabindex="0">84 kHz</li>
<li data-value="97000" class="option" tabindex="0">97 kHz</li>
<li data-value="114000" class="option" tabindex="0">114 kHz</li>
<li data-value="133000" class="option" tabindex="0">133 kHz</li>
<li data-value="151000" class="option" tabindex="0">151 kHz</li>
<li data-value="184000" class="option" tabindex="0">184 kHz</li>
<li data-value="200000" class="option" tabindex="0">200 kHz</li>
<li data-value="217000" class="option" tabindex="0">217 kHz</li>
<li data-value="236000" class="option" tabindex="0">236 kHz</li>
<li data-value="254000" class="option" tabindex="0">254 kHz</li>
<li data-value="287000" class="option" tabindex="0">287 kHz</li>
<li data-value="311000" class="option" tabindex="0">311 kHz</li>
</ul>
</div>
<% } %>
<% if (device == 'xdr' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" data-value2="-1" class="option" tabindex="0">Auto</li>
<li data-value="55000" data-value2="0" class="option" tabindex="0">55 kHz</li>
<li data-value="73000" data-value2="1" class="option" tabindex="0">73 kHz</li>
<li data-value="90000" data-value2="2" class="option" tabindex="0">90 kHz</li>
<li data-value="108000" data-value2="3" class="option" tabindex="0">108 kHz</li>
<li data-value="125000" data-value2="4" class="option" tabindex="0">125 kHz</li>
<li data-value="142000" data-value2="5" class="option" tabindex="0">142 kHz</li>
<li data-value="159000" data-value2="6" class="option" tabindex="0">159 kHz</li>
<li data-value="177000" data-value2="7" class="option" tabindex="0">177 kHz</li>
<li data-value="194000" data-value2="8" class="option" tabindex="0">194 kHz</li>
<li data-value="211000" data-value2="9" class="option" tabindex="0">211 kHz</li>
<li data-value="229000" data-value2="10" class="option" tabindex="0">229 kHz</li>
<li data-value="246000" data-value2="11" class="option" tabindex="0">246 kHz</li>
<li data-value="263000" data-value2="12" class="option" tabindex="0">263 kHz</li>
<li data-value="281000" data-value2="13" class="option" tabindex="0">281 kHz</li>
<li data-value="298000" data-value2="14" class="option" tabindex="0">298 kHz</li>
<li data-value="309000" data-value2="15" class="option" tabindex="0">309 kHz</li>
</ul>
</div>
<% } %>
<% if (device == 'sdr' && bwSwitch == true) { %>
<div class="panel-50 w-150 no-bg h-100 m-0 dropdown dropdown-up" id="data-bw" style="margin-left: 15px !important;">
<input type="text" placeholder="Auto BW" readonly tabindex="0">
<ul class="options open-top" tabindex="-1">
<li data-value="0" class="option" tabindex="0">Auto</li>
<li data-value="4000" class="option" tabindex="0">4 kHz</li>
<li data-value="8000" class="option" tabindex="0">8 kHz</li>
<li data-value="10000" class="option" tabindex="0">10 kHz</li>
<li data-value="20000" class="option" tabindex="0">20 kHz</li>
<li data-value="30000" class="option" tabindex="0">30 kHz</li>
<li data-value="50000" class="option" tabindex="0">50 kHz</li>
<li data-value="75000" class="option" tabindex="0">75 kHz</li>
<li data-value="100000" class="option" tabindex="0">100 kHz</li>
<li data-value="125000" class="option" tabindex="0">125 kHz</li>
<li data-value="150000" class="option" tabindex="0">150 kHz</li>
<li data-value="175000" class="option" tabindex="0">175 kHz</li>
<li data-value="200000" class="option" tabindex="0">200 kHz</li>
<li data-value="225000" class="option" tabindex="0">225 kHz</li>
</ul>
</div>
<% } %>
<% if (fmlist_integration == true) { %>
<button class="tooltip bg-color-4 popup" id="log-fmlist"
data-tooltip="<strong>LOG TO FMLIST</strong><br>Clicking this button logs the current station to FMLIST's visual logbook." aria-label="Log to FMLIST"
style="width: 80px; height: 48px;margin-left: 15px !important;">
<i class="fa-solid fa-flag fa-lg"></i>
<span class="popup-content">
Choose the DX propagation type:<br>
<a class="top-10 bg-color-3 text-bold" style="padding: 10px; border-radius: 15px 0 0 15px; display: inline-block;" id="log-fmlist-tropo">Tropo</a><!--
--><a class="top-10 bg-color-3 text-bold" style="padding: 10px; border-radius: 0 15px 15px 0; display: inline-block;" id="log-fmlist-sporadice">Sporadic-E</a>
</span>
</button>
<% } %>
</div>
</div>
</div>
<div class="flex-container flex-phone flex-phone-column">
<div class="panel-75 hover-brighten" id="rt-container" style="height: 100px;">
<h2 style="margin-top: 4px;">RADIOTEXT</h2>
<div id="data-rt0">
<span></span>
</div>
<div id="data-rt1">
<span></span>
</div>
<hr class="hide-desktop">
</div>
<div class="panel-33 hover-brighten tooltip" 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">
<span id="data-station-name"></span>
</h2>
<h4 class="m-0">
<span id="data-station-city" style="font-size: 16px;"></span> <span class="text-small">[<span id="data-station-itu"></span>]</span>
</h4>
<span class="text-small">
<span id="data-station-erp"></span> kW [<span id="data-station-pol"></span>] <span class="text-gray">•</span> <span id="data-station-distance"></span> <span class="text-gray">•</span> <span id="data-station-azimuth"></span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-10 no-bg" style="margin-left: 0; margin-top: 0; margin-right: 0;display:flex;">
<div class="panel-100" style="margin-left: 0;">
<h2 class="bottom-10">AF</h2>
<div id="af-list" style="text-align: center;">
<ul> </ul>
</div>
</div>
</div>
</div>
<div id="flags-container-phone" class="panel-33">
<h2 class="show-phone">
<div class="data-pty text-color-default"></div>
</h2>
<h3 style="margin-top:0;margin-bottom:0;" class="colornode-4 flex-center">
<span class="data-tp">TP</span>
<span style="margin-left: 15px;" class="data-ta">TA</span>
<div style="display:inline-block">
<span style="margin-left: 20px;display: block;margin-top: 2px;" class="data-flag"></span>
</div>
<span class="pointer stereo-container" style="position: noderelative; margin-left: 20px;" role="button" aria-label="Stereo / Mono toggle" tabindex="0">
<div class="circle-container">
<div class="circle data-st circle1"></div>
<div class="circle data-st circle2"></div>
</div>
<span class="overlay tooltip" data-tooltip="Stereo / Mono toggle. <br><strong>Click to toggle."></span>
</span>
<span style="margin-left: 15px;" class="data-ms">MS</span>
</h3>
</div>
</div>
<div id="myModal" class="modal">
<div class="modal-panel">
<div class="flex-container flex-phone" style="height: calc(100% - 100px)">
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close settings" tabindex="0"><i class="fa-solid fa-chevron-right"></i></div>
<div class="modal-panel-content">
<h1 class="top-25">Settings</h1>
<div class="panel-full flex-center no-bg m-0">
<%- include('_components', { component: 'dropdown', id: 'theme-selector', inputId: 'theme-selector-input', label: 'Theme', cssClass: '', placeholder: 'Default',
options: [
{ value: 'theme1', label: 'Mint' },
{ value: 'theme2', label: 'Cappuccino' },
{ value: 'theme3', label: 'Nature' },
{ value: 'theme4', label: 'Ocean' },
{ value: 'theme5', label: 'Terminal' },
{ value: 'theme6', label: 'Nightlife' },
{ value: 'theme7', label: 'Blurple' },
{ value: 'theme8', label: 'Construction' },
{ value: 'theme9', label: 'Amoled' },
]
}) %>
</div>
<% if (device !== 'sdr') { %>
<div class="panel-full flex-center no-bg m-0">
<%- include('_components', { component: 'dropdown', id: 'signal-selector', inputId: 'signal-selector-input', label: 'Signal units', cssClass: '', placeholder: 'dBf',
options: [
{ value: 'dbf', label: 'dBf' },
{ value: 'dbuv', label: 'dBuV' },
{ value: 'dbm', label: 'dBm' },
]
}) %>
</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="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">
<div class="icon tooltip <% if (tunerLock) { %>active<% } %>" id="dashboard-lock-admin" onClick="toggleLock('#dashboard-lock-admin', 'wL1', 'wL0', 'Unlock Tuner (Admin)', 'Lock Tuner (Admin)');" role="button" aria-label="Toggle admin lock until restart" tabindex="0" data-tooltip="Toggle admin lock<br>Lasts until restart">
<i class="fa-solid fa-lock"></i>
</div>
<div class="icon tooltip <% if (!publicTuner) { %>active<% } %>" id="dashboard-lock-tune" onClick="toggleLock('#dashboard-lock-tune', 'wT1', 'wT0', 'Unlock Tuner (Password tune)', 'Lock Tuner (Password tune)');" role="button" aria-label="Toggle password lock until restart" tabindex="0" data-tooltip="Toggle password lock<br>Lasts until restart">
<i class="fa-solid fa-key"></i>
</div>
<div class="icon tooltip" role="button" aria-label="Go to admin panel" tabindex="0" data-tooltip="Go to admin panel" onClick="window.open('./setup', '_blank').focus();">
<i class="fa-solid fa-user"></i>
</div>
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
<i class="fa-solid fa-sign-out"></i>
</div>
</div>
<% } else if (isTuneAuthenticated) { %>
<p class="color-3">You are logged in and can control the receiver.</p>
<div class="admin-quick-dashboard">
<div class="icon tooltip <% if (!publicTuner) { %>active<% } %>" id="dashboard-lock-tune" onClick="toggleLock('#dashboard-lock-tune', 'wT1', 'wT0', 'Unlock Tuner (Password tune)', 'Lock Tuner (Password tune)');" role="button" aria-label="Toggle password lock until disconnect" tabindex="0" data-tooltip="Toggle password lock<br>Lasts until disconnect">
<i class="fa-solid fa-key"></i>
</div>
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
<i class="fa-solid fa-sign-out"></i>
</div>
</div>
<% } else { %>
<form action="./login" method="post" id="login-form" class="top-25">
<input type="password" id="password" name="password" placeholder="Password" style="width: 145px; border-radius: 15px 0 0 15px" required>
<button type="submit" class="br-0 top-10 tooltip" style="height: 46px; width: 50px; margin-left: -2px;border-radius: 0 15px 15px 0;" role="button" aria-label="Log in" tabindex="0" data-tooltip="Log in">
<i class="fa-solid fa-right-to-bracket"></i>
</button>
</form>
<% } %>
<div id="login-message" class="color-3">&nbsp;</div>
<div class="version-info">
<p class="m-0">
FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span>
</p>
<p class="text-small m-0 color-3">by <a href="https://fmdx.org" target="_blank">FMDX.org</a></p>
<span class="text-small" style="color: var(--color-3);">[<a href="https://servers.fmdx.org/" target="_blank">Receiver Map</a>]</span>
<br>
<p class="text-small color-3" id="current-ping"></p>
</div>
</div>
</div>
<div class="modal-panel-footer flex-container flex-phone">
<div class="modal-panel-sidebar" style="font-size: 22px;">
<div class="flex-center" style="height: 50px">
<i class="fa-solid fa-hand-holding-medical"></i>
</div>
<div class="flex-center" style="height: 50px">
<i class="fa-brands fa-discord"></i>
</div>
</div>
<div class="modal-panel-content">
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://buymeacoffee.com/noobish')">
<strong>Support</strong> the developer!
</div>
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://discord.com/invite/ZAVNdS74mC')">
Join our <strong>FMDX.org Discord</strong> community!
</div>
</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>
<script src="js/websocket.js"></script>
<script src="js/webserver.js"></script>
<% if (!noPlugins) { %>
<% plugins?.forEach(function(plugin) { %>
<script src="js/plugins/<%= plugin %>"></script>
<% }); %>
<% } %>
</body>
</html>

View File

@@ -49,7 +49,7 @@ $(document).ready(function() {
chatMessageCount++;
chatMessagesCount.text(chatMessageCount);
chatMessagesCount.attr("aria-label", "Chat (" + chatMessageCount + " unread)");
chatButton.removeClass('bg-color-2').addClass('blink');
chatButton.removeClass('bg-color-1').addClass('blink');
}
}
}
@@ -67,7 +67,7 @@ $(document).ready(function() {
chatButton.click(function() {
chatMessageCount = 0;
chatMessagesCount.text(chatMessageCount);
chatButton.removeClass('blink').addClass('bg-color-2');
chatButton.removeClass('blink').addClass('bg-color-1');
chatSendInput.focus();
setTimeout(function() {

View File

@@ -1,9 +1,9 @@
var currentDate = new Date('Feb 9, 2025 18:00:00');
var currentDate = new Date('Feb 16, 2025 15:00: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.4 [' + formattedDate + ']';
var currentVersion = 'v1.3.5 [' + formattedDate + ']';
getInitialSettings();
removeUrlParameters();

File diff suppressed because it is too large Load Diff

61
web/js/plugins.js Normal file
View File

@@ -0,0 +1,61 @@
function checkScroll() {
let $container = $(".scrollable-container");
let $leftArrow = $(".scroll-left");
let $rightArrow = $(".scroll-right");
let scrollWidth = $container[0].scrollWidth;
let clientWidth = $container[0].clientWidth;
let scrollLeft = $container.scrollLeft();
let maxScrollLeft = scrollWidth - clientWidth;
if (scrollWidth > clientWidth) {
// If scrolling is possible, show arrows
$leftArrow.stop(true, true).fadeIn(200).css("pointer-events", scrollLeft > 0 ? "auto" : "none").fadeTo(200, scrollLeft > 0 ? 1 : 0.2);
$rightArrow.stop(true, true).fadeIn(200).css("pointer-events", scrollLeft < maxScrollLeft ? "auto" : "none").fadeTo(200, scrollLeft < maxScrollLeft ? 1 : 0.2);
} else {
// No scrolling needed, fully hide arrows
$leftArrow.stop(true, true).fadeOut(200);
$rightArrow.stop(true, true).fadeOut(200);
}
}
$(document).ready(function () {
let $container = $(".scrollable-container");
let $leftArrow = $(".scroll-left");
let $rightArrow = $(".scroll-right");
// Scroll left/right when arrows are clicked
$leftArrow.on("click", function () {
$container.animate({ scrollLeft: "-=100" }, 300);
});
$rightArrow.on("click", function () {
$container.animate({ scrollLeft: "+=100" }, 300);
});
// Detect scrolling
$container.on("scroll", checkScroll);
// Run checkScroll on page load to adjust visibility
setTimeout(checkScroll, 100);
});
// Function to add buttons dynamically
function addIconToPluginPanel(id, text, iconType, icon, tooltip) {
let $pluginButton = $(`
<button class="no-bg color-4 hover-brighten ${tooltip ? "tooltip" : ""}"
style="padding: 6px; width: 64px; min-width: 64px;" id="${id}"
data-tooltip="${tooltip ? tooltip : ""}" data-tooltip-placement="bottom">
<i class="fa-${iconType} fa-${icon} fa-lg top-10"></i><br>
<span style="font-size: 10px; color: var(--color-main-bright) !important;">${text}</span>
</button>
`);
$('.scrollable-container').append($pluginButton);
initTooltips($pluginButton);
// Recheck scrolling when new buttons are added
setTimeout(checkScroll, 100);
}

View File

@@ -209,7 +209,7 @@ function setTheme(themeName) {
$(':root').css('--color-main-bright', themeColors[1]);
$(':root').css('--color-text', themeColors[2]);
$(':root').css('--color-text-2', textColor2);
$('#wrapper-outer').css('background-color', backgroundColorWithOpacity);
$('.wrapper-outer').css('background-color', backgroundColorWithOpacity);
}
}

View File

@@ -4,4 +4,5 @@ $.getScript('./js/dropdown.js');
$.getScript('./js/modal.js');
$.getScript('./js/settings.js');
$.getScript('./js/chat.js');
$.getScript('./js/toast.js');
$.getScript('./js/toast.js');
$.getScript('./js/plugins.js');

View File

@@ -13,7 +13,7 @@
</head>
<body>
<div id="toast-container"></div>
<div id="wrapper-outer">
<div class="wrapper-outer wrapper-full">
<div id="wrapper">
<div class="panel-100 no-bg">
<img class="top-25" src="favicon.png" height="64px">

View File

@@ -13,7 +13,7 @@
</head>
<body>
<div id="toast-container"></div>
<div id="wrapper-outer" class="wrapper-outer-static">
<div class="wrapper-outer wrapper-full wrapper-outer-static">
<div id="navigation" class="sidenav flex-container flex-phone">
<div class="sidenav-content">
<h1 class="top-25">Settings</h1>

View File

@@ -13,7 +13,7 @@
</head>
<body>
<div id="toast-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
<div id="wrapper-outer">
<div class="wrapper-outer wrapper-full">
<div id="wrapper">
<div class="panel-100 no-bg">
<img class="top-10" src="../images/openradio_logo_neutral.png" height="64px">