You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 22:13:53 +01:00
chat, theme changes, bugfixes
This commit is contained in:
128
index.js
128
index.js
@@ -16,6 +16,7 @@ const process = require("process");
|
||||
// Websocket handling
|
||||
const WebSocket = require('ws');
|
||||
const wss = new WebSocket.Server({ noServer: true });
|
||||
const chatWss = new WebSocket.Server({ noServer: true });
|
||||
const path = require('path');
|
||||
const net = require('net');
|
||||
const client = new net.Socket();
|
||||
@@ -31,6 +32,19 @@ const audioStream = require('./stream/index.js');
|
||||
const { parseAudioDevice } = require('./stream/parser.js');
|
||||
const { configName, serverConfig, configUpdate, configSave } = require('./server_config');
|
||||
const { logDebug, logError, logInfo, logWarn } = consoleCmd;
|
||||
var pjson = require('./package.json');
|
||||
|
||||
console.log(`\x1b[32m
|
||||
_____ __ __ ______ __ __ __ _
|
||||
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
|
||||
| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__|
|
||||
| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ |
|
||||
|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_|
|
||||
`);
|
||||
console.log('\x1b[0mFM-DX-Webserver', pjson.version);
|
||||
console.log('\x1b[90m======================================================');
|
||||
|
||||
|
||||
|
||||
// Create a WebSocket proxy instance
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
@@ -40,6 +54,7 @@ const proxy = httpProxy.createProxyServer({
|
||||
});
|
||||
|
||||
let currentUsers = 0;
|
||||
let connectedUsers = [];
|
||||
let streamEnabled = false;
|
||||
let incompleteDataBuffer = '';
|
||||
|
||||
@@ -225,7 +240,8 @@ app.get('/static_data', (req, res) => {
|
||||
res.json({
|
||||
qthLatitude: serverConfig.identification.lat,
|
||||
qthLongitude: serverConfig.identification.lon,
|
||||
streamEnabled: streamEnabled
|
||||
streamEnabled: streamEnabled,
|
||||
presets: serverConfig.webserver.presets || []
|
||||
});
|
||||
});
|
||||
|
||||
@@ -325,7 +341,11 @@ app.get('/', (req, res) => {
|
||||
tunerDescMeta: removeMarkdown(serverConfig.identification.tunerDesc),
|
||||
tunerLock: serverConfig.lockToAdmin,
|
||||
publicTuner: serverConfig.publicTuner,
|
||||
antennaSwitch: serverConfig.antennaSwitch
|
||||
ownerContact: serverConfig.identification.contact,
|
||||
antennaSwitch: serverConfig.antennaSwitch,
|
||||
tuningLimit: serverConfig.webserver.tuningLimit,
|
||||
tuningLowerLimit: serverConfig.webserver.tuningLowerLimit,
|
||||
tuningUpperLimit: serverConfig.webserver.tuningUpperLimit
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -351,7 +371,8 @@ app.get('/setup', (req, res) => {
|
||||
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
|
||||
processUptime: formattedProcessUptime,
|
||||
consoleOutput: consoleCmd.logs,
|
||||
onlineUsers: dataHandler.dataToSend.users
|
||||
onlineUsers: dataHandler.dataToSend.users,
|
||||
connectedUsers: connectedUsers
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -475,9 +496,17 @@ wss.on('connection', (ws, request) => {
|
||||
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.country === undefined) {
|
||||
const userData = { ip: clientIp, location: 'Unknown', time: connectionTime };
|
||||
connectedUsers.push(userData);
|
||||
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
|
||||
} else {
|
||||
const userLocation = `${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`;
|
||||
const userData = { ip: clientIp, location: userLocation, time: connectionTime };
|
||||
connectedUsers.push(userData);
|
||||
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -495,6 +524,14 @@ wss.on('connection', (ws, request) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if(command.startsWith('T')) {
|
||||
let tuneFreq = Number(command.slice(1)) / 1000;
|
||||
|
||||
if(serverConfig.webserver.tuningLimit === true && (tuneFreq < serverConfig.webserver.tuningLowerLimit || tuneFreq > serverConfig.webserver.tuningUpperLimit)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if((serverConfig.publicTuner === true) || (request.session && request.session.isTuneAuthenticated === true)) {
|
||||
|
||||
if(serverConfig.lockToAdmin === true) {
|
||||
@@ -512,15 +549,85 @@ wss.on('connection', (ws, request) => {
|
||||
ws.on('close', (code, reason) => {
|
||||
currentUsers--;
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
if(currentUsers === 0 && serverConfig.autoShutdown === true) {
|
||||
client.write('X\n');
|
||||
|
||||
// Find the index of the user's data in connectedUsers array
|
||||
const index = connectedUsers.findIndex(user => user.ip === clientIp);
|
||||
if (index !== -1) {
|
||||
connectedUsers.splice(index, 1); // Remove the user's data from connectedUsers array
|
||||
}
|
||||
|
||||
if (currentUsers === 0 && serverConfig.autoShutdown === true) {
|
||||
client.write('X\n');
|
||||
}
|
||||
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('error', console.error);
|
||||
});
|
||||
|
||||
// CHAT WEBSOCKET BLOCK
|
||||
// Assuming chatWss is your WebSocket server instance
|
||||
// Initialize an array to store chat messages
|
||||
let chatHistory = [];
|
||||
|
||||
chatWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
|
||||
// Send chat history to the newly connected client
|
||||
chatHistory.forEach(function(message) {
|
||||
message.history = true; // Adding the history parameter
|
||||
ws.send(JSON.stringify(message));
|
||||
});
|
||||
|
||||
const ipMessage = {
|
||||
type: 'clientIp',
|
||||
ip: clientIp,
|
||||
admin: request.session.isAdminAuthenticated
|
||||
};
|
||||
ws.send(JSON.stringify(ipMessage));
|
||||
|
||||
ws.on('message', function incoming(message) {
|
||||
const messageData = JSON.parse(message);
|
||||
messageData.ip = clientIp; // Adding IP address to the message object
|
||||
const currentTime = new Date();
|
||||
|
||||
const hours = String(currentTime.getHours()).padStart(2, '0');
|
||||
const minutes = String(currentTime.getMinutes()).padStart(2, '0');
|
||||
messageData.time = `${hours}:${minutes}`; // Adding current time to the message object in hours:minutes format
|
||||
|
||||
if (serverConfig.webserver.banlist.includes(clientIp)) {
|
||||
return; // Do not proceed further if banned
|
||||
}
|
||||
|
||||
if(request.session.isAdminAuthenticated === true) {
|
||||
messageData.admin = true;
|
||||
}
|
||||
|
||||
// Limit message length to 255 characters
|
||||
if (messageData.message.length > 255) {
|
||||
messageData.message = messageData.message.substring(0, 255);
|
||||
}
|
||||
|
||||
// Add the new message to chat history and keep only the latest 50 messages
|
||||
chatHistory.push(messageData);
|
||||
if (chatHistory.length > 50) {
|
||||
chatHistory.shift(); // Remove the oldest message if the history exceeds 50 messages
|
||||
}
|
||||
|
||||
const modifiedMessage = JSON.stringify(messageData);
|
||||
|
||||
chatWss.clients.forEach(function each(client) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(modifiedMessage);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('close', function close() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Handle upgrade requests to /text and proxy /audio WebSocket connections
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
if (request.url === '/text') {
|
||||
@@ -531,11 +638,16 @@ httpServer.on('upgrade', (request, socket, head) => {
|
||||
});
|
||||
} else if (request.url === '/audio') {
|
||||
proxy.ws(request, socket, head);
|
||||
} else if (request.url === '/chat') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
chatWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/* Serving of HTML files */
|
||||
app.use(express.static(path.join(__dirname, 'web')));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fm-dx-webserver",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -13,7 +13,8 @@ if (index !== -1 && index + 1 < process.argv.length) {
|
||||
let serverConfig = {
|
||||
webserver: {
|
||||
webserverIp: "0.0.0.0",
|
||||
webserverPort: 8080
|
||||
webserverPort: 8080,
|
||||
banlist: []
|
||||
},
|
||||
xdrd: {
|
||||
xdrdIp: "127.0.0.1",
|
||||
@@ -55,9 +56,16 @@ function deepMerge(target, source)
|
||||
}
|
||||
|
||||
function configUpdate(newConfig) {
|
||||
if (newConfig.webserver && newConfig.webserver.banlist !== undefined) {
|
||||
// If new banlist is provided, replace the existing one
|
||||
serverConfig.webserver.banlist = newConfig.webserver.banlist;
|
||||
delete newConfig.webserver.banlist; // Remove banlist from newConfig to avoid merging
|
||||
}
|
||||
|
||||
deepMerge(serverConfig, newConfig);
|
||||
}
|
||||
|
||||
|
||||
function configSave() {
|
||||
fs.writeFile(configName + '.json', JSON.stringify(serverConfig, null, 2), (err) => {
|
||||
if (err) {
|
||||
|
||||
@@ -102,6 +102,23 @@ label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chatbutton.hide-desktop {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--color-text);
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
font-size: 16px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
transition: 500ms ease-in-out background;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#settings:hover, #back-btn:hover, #users-online-container:hover {
|
||||
background: var(--color-3);
|
||||
}
|
||||
@@ -199,6 +216,9 @@ label {
|
||||
canvas, #flags-container {
|
||||
display: none;
|
||||
}
|
||||
#tuner-desc {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
#ps-container {
|
||||
background-color: var(--color-1);
|
||||
height: 100px !important;
|
||||
@@ -213,8 +233,8 @@ label {
|
||||
}
|
||||
#data-pi {
|
||||
font-size: 24px;
|
||||
margin-top: 50px;
|
||||
color: var(--color-text-2)
|
||||
margin-top: 20px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
h2.show-phone {
|
||||
display: inline;
|
||||
@@ -230,6 +250,7 @@ label {
|
||||
font-size: 10px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
#rt-container {
|
||||
height: 32px !important;
|
||||
@@ -272,6 +293,13 @@ label {
|
||||
.tuner-info {
|
||||
margin-bottom: -60px !important;
|
||||
}
|
||||
#af-list ul {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#rt-container {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) and (max-height: 860px) {
|
||||
@@ -287,6 +315,12 @@ label {
|
||||
.tuner-info #tuner-name {
|
||||
float: left;
|
||||
font-size: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tuner-info #tuner-limit {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tuner-info #tuner-desc {
|
||||
@@ -303,6 +337,9 @@ label {
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
#af-list ul {
|
||||
max-height: 330px;
|
||||
height: 225px !important;
|
||||
}
|
||||
.chatbutton {
|
||||
height: 86px !important;
|
||||
}
|
||||
}
|
||||
@@ -64,15 +64,6 @@ body {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
#wrapper {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin: 50px auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--color-text-2);
|
||||
@@ -80,4 +71,52 @@ a {
|
||||
|
||||
a:hover {
|
||||
border-bottom: 1px solid var(--color-4);
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--color-4);
|
||||
}
|
||||
|
||||
table {
|
||||
border-radius: 30px;
|
||||
background-color: var(--color-2);
|
||||
padding: 20px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
table th {
|
||||
padding: 8px 20px;
|
||||
outline: 1px solid var(--color-3);
|
||||
background-color: var(--color-3);
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
table td:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table td:nth-child(2) {
|
||||
color: var(--color-main-bright);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
table th:nth-child(1) {
|
||||
border-radius: 30px 0px 0px 30px;
|
||||
}
|
||||
|
||||
table th:nth-last-child(1){
|
||||
border-radius: 0px 30px 30px 0px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
#wrapper {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin: 50px auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
display: none;
|
||||
background-color: var(--color-main);
|
||||
}
|
||||
|
||||
@@ -122,6 +123,26 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-panel-chat {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
height: 450px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
display: none;
|
||||
background-color: var(--color-main);
|
||||
border-radius: 30px 30px 0px 0px;
|
||||
}
|
||||
|
||||
.modal-panel-chat .modal-panel-sidebar {
|
||||
width: 100%;
|
||||
border-radius: 30px 30px 0px 0px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.modal-content {
|
||||
min-width: 90% !important;
|
||||
@@ -134,12 +155,18 @@
|
||||
.modal-title {
|
||||
position: static;
|
||||
}
|
||||
#closeModalButton {
|
||||
.closeModalButton {
|
||||
position: static;
|
||||
}
|
||||
.modal-panel {
|
||||
width: 100%;
|
||||
}
|
||||
.modal-panel-chat {
|
||||
height: 500px;
|
||||
}
|
||||
#chat-chatbox {
|
||||
height: 333px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 768px) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
.panel-10 {
|
||||
width: 10%;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.panel-33 {
|
||||
@@ -44,9 +44,9 @@
|
||||
padding: 20px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.panel-100 {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
.panel-100, .panel-100.w-100 {
|
||||
width: 90% !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
[class^="panel-"] {
|
||||
margin: auto;
|
||||
@@ -67,11 +67,10 @@
|
||||
*[class^="panel-"] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.panel-10, .panel-90 {
|
||||
.panel-90 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.panel-10 {
|
||||
margin: 0;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,14 @@
|
||||
<div id="wrapper">
|
||||
<div class="panel-100 no-bg tuner-info">
|
||||
<h1 id="tuner-name"><%= tunerName %> <% if (!publicTuner) { %><i class="fa-solid fa-key pointer" title="Only people with tune password can tune."></i>
|
||||
<% } else if (tunerLock) { %><i class="fa-solid fa-lock pointer" title="Tuner is currently locked to admin."></i><% } %></h1>
|
||||
<p id="tuner-desc"><%- tunerDesc %></p>
|
||||
<% } else if (tunerLock) { %><i class="fa-solid fa-lock pointer" title="Tuner is currently locked to admin."></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">
|
||||
@@ -61,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-90 bg-none">
|
||||
<div class="panel-100 bg-none">
|
||||
<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;">
|
||||
@@ -148,17 +154,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-75 hover-brighten" id="rt-container" style="height: 110px;">
|
||||
<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"></div>
|
||||
<div id="data-rt1"></div>
|
||||
<div id="data-container" style="display: none;"></div>
|
||||
<hr class="hide-desktop">
|
||||
</div>
|
||||
|
||||
<div class="panel-33 hover-brighten">
|
||||
<div id="data-station-container">
|
||||
<h2 style="margin-top: 4px;" class="mb-0">
|
||||
<h2 style="margin-top: 0;" class="mb-0">
|
||||
<span id="data-station-name"></span>
|
||||
</h2>
|
||||
<h4 class="m-0">
|
||||
@@ -174,14 +180,17 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-10 bg-none">
|
||||
<div class="panel-100" style="height: 100%;">
|
||||
<div class="panel-100 w-100">
|
||||
<h2>AF</h2>
|
||||
<div id="af-list" style="text-align: center;">
|
||||
<ul>
|
||||
<div id="af-list" class="p-bottom-20" style="text-align: center;">
|
||||
<ul style="height: 251px;">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-10 no-bg h-100 hide-phone" style="width: 100px; height: 100px;">
|
||||
<button class="chatbutton bg-color-2"><i class="fa-solid fa-comments fa-lg"></i> (<span class="chat-messages-count">0</span>)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="flags-container-phone" class="panel-33">
|
||||
@@ -201,12 +210,13 @@
|
||||
</div>
|
||||
|
||||
<button id="settings" aria-label="Settings"><i class="fa-solid fa-gear"></i></button>
|
||||
<button class="chatbutton hide-desktop bg-color-2"><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" id="closeModal"><i class="fa-solid fa-chevron-right"></i></div>
|
||||
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal"><i class="fa-solid fa-chevron-right"></i></div>
|
||||
<div class="modal-panel-content">
|
||||
<h1 class="top-25">Settings</h1>
|
||||
|
||||
@@ -216,13 +226,13 @@
|
||||
<input type="text" placeholder="Theme" readonly />
|
||||
<ul class="options">
|
||||
<li class="option" data-value="theme1">Default</li>
|
||||
<li class="option" data-value="theme2">Red</li>
|
||||
<li class="option" data-value="theme3">Green</li>
|
||||
<li class="option" data-value="theme4">Cyan</li>
|
||||
<li class="option" data-value="theme5">Orange</li>
|
||||
<li class="option" data-value="theme6">Pink</li>
|
||||
<li class="option" data-value="theme2">Cappuccino</li>
|
||||
<li class="option" data-value="theme3">Nature</li>
|
||||
<li class="option" data-value="theme4">Ocean</li>
|
||||
<li class="option" data-value="theme5">Terminal</li>
|
||||
<li class="option" data-value="theme6">Nightlife</li>
|
||||
<li class="option" data-value="theme7">Blurple</li>
|
||||
<li class="option" data-value="theme8">Bee</li>
|
||||
<li class="option" data-value="theme8">Construction</li>
|
||||
<li class="option" data-value="theme9">AMOLED</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -283,6 +293,12 @@
|
||||
<p class="text-small">FM-DX WebServer <br>by <a href="https://noobish.eu" target="_blank">Noobish</a>, <a href="https://fmdx.pl" target="_blank">kkonradpl</a> & the OpenRadio community.</p>
|
||||
<span style="color: var(--color-3);" class="version-string"></span><br>
|
||||
<span class="text-small" style="color: var(--color-3);">[<a href="https://list.fmdx.pl" target="_blank">Receiver Map</a>]</span>
|
||||
<br>
|
||||
<br>
|
||||
<% if(ownerContact){ %>
|
||||
<span>Owner contact:<span><br>
|
||||
<span class="text-small m-0"><%= ownerContact %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,7 +321,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-panel-chat">
|
||||
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal"><i class="fa-solid fa-chevron-down"></i></div>
|
||||
<div class="modal-panel-content text-left">
|
||||
<div style="text-align: center;">
|
||||
<input type="text" id="chat-nickname" name="chat-nickname" placeholder="Nickname">
|
||||
<button class="br-0 w-100 top-10" style="height: 44px" id="chat-nickname-save">Save</button>
|
||||
<p style="margin: 5px;">
|
||||
Current identity: <span style="color: lime;" id="chat-admin"></span> <strong id="chat-identity-nickname">Anonymous User</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="chat-chatbox" class="bg-color-1" style="height: 270px;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 class="chat-send-message-btn br-0" style="width: 80px; height: 45px;"><i class="fa-solid fa-paper-plane"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/webserver.js"></script>
|
||||
|
||||
101
web/js/chat.js
Normal file
101
web/js/chat.js
Normal file
@@ -0,0 +1,101 @@
|
||||
var chatUrl = new URL('chat', window.location.href);
|
||||
chatUrl.protocol = chatUrl.protocol.replace('http', 'ws');
|
||||
var chatSocketAddress = chatUrl.href;
|
||||
var chatSocket = new WebSocket(chatSocketAddress);
|
||||
let chatMessageCount = 0;
|
||||
|
||||
$(document).ready(function() {
|
||||
chatSocket.onopen = function() {
|
||||
};
|
||||
|
||||
chatSocket.onmessage = function(event) {
|
||||
var messages = $('#chat-chatbox');
|
||||
let messageData = JSON.parse(event.data); // Parse event.data to access its properties
|
||||
|
||||
let isAdmin = messageData.admin ? '<span style="color: lime">[ADMIN]</span>' : ''; // Add '[ADMIN] ' if messageData.admin is true, otherwise empty string
|
||||
// Check if the message type is 'clientIp'
|
||||
if (messageData.type === 'clientIp') {
|
||||
// Fill the client IP into the element with ID #chat-ip
|
||||
$('#chat-admin').html(isAdmin);
|
||||
$('#chat-identity-nickname').attr('title', messageData.ip)
|
||||
} else {
|
||||
let chatMessage = `
|
||||
<span class="color-2">[${messageData.time}]</span>
|
||||
${isAdmin} <strong class="color-5" title="IP Address: ${messageData.ip}">${messageData.nickname}</strong>: <span style="color: var(--color-text-2);">${$('<div/>').text(messageData.message).html()}</span><br>
|
||||
`;
|
||||
|
||||
messages.append(chatMessage);
|
||||
|
||||
if($('#chat-chatbox').is(':visible')) {
|
||||
$('#chat-chatbox').scrollTop($('#chat-chatbox')[0].scrollHeight);
|
||||
} else {
|
||||
if(messageData.history !== true) {
|
||||
chatMessageCount++;
|
||||
$('.chat-messages-count').text(chatMessageCount);
|
||||
$('.chatbutton').removeClass('bg-color-2').addClass('bg-color-4');
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(messageData);
|
||||
};
|
||||
|
||||
$('.chat-send-message-btn').click(function() {
|
||||
sendMessage();
|
||||
});
|
||||
|
||||
$('#chat-nickname-save').click(function() {
|
||||
let currentNickname = $('#chat-nickname').val();
|
||||
localStorage.setItem('nickname', currentNickname);
|
||||
$('#chat-identity-nickname').text(localStorage.getItem('nickname'));
|
||||
$('#chat-nickname').blur();
|
||||
});
|
||||
|
||||
$('.chatbutton').click(function() {
|
||||
$('#chat-chatbox').scrollTop($('#chat-chatbox')[0].scrollHeight);
|
||||
chatMessageCount = 0;
|
||||
$('#chat-messages-count').text(chatMessageCount);
|
||||
$('.chatbutton').removeClass('bg-color-4').addClass('bg-color-2');
|
||||
$('#chat-send-message').focus();
|
||||
});
|
||||
|
||||
$('#chat-nickname').keypress(function(event) {
|
||||
if (event.which == 13) { // 13 is the keycode for Enter key
|
||||
$('#chat-nickname-save').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$('#chat-send-message').keypress(function(event) {
|
||||
if (event.which == 13) { // 13 is the keycode for Enter key
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
if(localStorage.getItem('nickname').length > 0) {
|
||||
$('#chat-nickname').val(localStorage.getItem('nickname'));
|
||||
$('#chat-identity-nickname').text(localStorage.getItem('nickname'));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
var input = $('#chat-send-message');
|
||||
var nickname = localStorage.getItem('nickname');
|
||||
if (nickname && nickname.length > 1) {
|
||||
// Only assign the nickname if it exists in localStorage and is longer than one character
|
||||
nickname = nickname;
|
||||
} else {
|
||||
// Otherwise, use the default nickname
|
||||
nickname = 'Anonymous user';
|
||||
}
|
||||
|
||||
|
||||
if (input.val().trim() !== '') {
|
||||
var messageData = {
|
||||
nickname: nickname,
|
||||
message: input.val()
|
||||
};
|
||||
|
||||
chatSocket.send(JSON.stringify(messageData));
|
||||
input.val('');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
function submitData() {
|
||||
const webserverIp = $('#webserver-ip').val() || '0.0.0.0';
|
||||
const webserverPort = $('#webserver-port').val() || '8080';
|
||||
const tuningLimit = $('#tuning-limit').is(":checked") || false;
|
||||
const tuningLowerLimit = $('#tuning-lower-limit').val() || '0';
|
||||
const tuningUpperLimit = $('#tuning-upper-limit').val() || '108';
|
||||
let presets = [];
|
||||
presets.push($('#preset1').val() || '87.5');
|
||||
presets.push($('#preset2').val() || '87.5');
|
||||
presets.push($('#preset3').val() || '87.5');
|
||||
presets.push($('#preset4').val() || '87.5');
|
||||
|
||||
let banlist = [];
|
||||
validateAndAdd(banlist);
|
||||
|
||||
const xdrdIp = $('#xdrd-ip').val() || '127.0.0.1';
|
||||
const xdrdPort = $('#xdrd-port').val() || '7373';
|
||||
@@ -34,6 +45,11 @@ function submitData() {
|
||||
webserver: {
|
||||
webserverIp,
|
||||
webserverPort,
|
||||
tuningLimit,
|
||||
tuningLowerLimit,
|
||||
tuningUpperLimit,
|
||||
presets,
|
||||
banlist
|
||||
},
|
||||
xdrd: {
|
||||
xdrdIp,
|
||||
@@ -76,6 +92,7 @@ function submitData() {
|
||||
data: JSON.stringify(data),
|
||||
success: function (message) {
|
||||
alert(message);
|
||||
console.log(data);
|
||||
},
|
||||
error: function (error) {
|
||||
console.error(error);
|
||||
@@ -96,6 +113,18 @@ function submitData() {
|
||||
.then(data => {
|
||||
$('#webserver-ip').val(data.webserver.webserverIp);
|
||||
$('#webserver-port').val(data.webserver.webserverPort);
|
||||
$('#tuning-limit').prop("checked", data.webserver.tuningLimit);
|
||||
$('#tuning-lower-limit').val(data.webserver.tuningLowerLimit || "");
|
||||
$('#tuning-upper-limit').val(data.webserver.tuningUpperLimit || "");
|
||||
|
||||
if(Array.isArray(data.webserver.presets)) {
|
||||
$('#preset1').val(data.webserver.presets[0] || "");
|
||||
$('#preset2').val(data.webserver.presets[1] || "");
|
||||
$('#preset3').val(data.webserver.presets[2] || "");
|
||||
$('#preset4').val(data.webserver.presets[3] || "");
|
||||
}
|
||||
|
||||
$('#ip-addresses').val(data.webserver.banlist?.join('\n') || "");
|
||||
|
||||
$('#xdrd-ip').val(data.xdrd.xdrdIp);
|
||||
$('#xdrd-port').val(data.xdrd.xdrdPort);
|
||||
@@ -143,3 +172,18 @@ function submitData() {
|
||||
console.error('Error fetching data:', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function validateAndAdd(banlist) {
|
||||
var textarea = $('#ip-addresses');
|
||||
var ipAddresses = textarea.val().split('\n');
|
||||
|
||||
// Regular expression to validate IP address
|
||||
var ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
|
||||
ipAddresses.forEach(function(ip) {
|
||||
if (ipRegex.test(ip)) {
|
||||
banlist.push(ip);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -8,8 +8,11 @@ function getInitialSettings() {
|
||||
// Use the received data (data.qthLatitude, data.qthLongitude) as needed
|
||||
localStorage.setItem('qthLatitude', data.qthLatitude);
|
||||
localStorage.setItem('qthLongitude', data.qthLongitude);
|
||||
localStorage.setItem('audioPort', data.audioPort);
|
||||
localStorage.setItem('streamEnabled', data.streamEnabled);
|
||||
localStorage.setItem('preset1', data.presets[0]);
|
||||
localStorage.setItem('preset2', data.presets[1]);
|
||||
localStorage.setItem('preset3', data.presets[2]);
|
||||
localStorage.setItem('preset4', data.presets[3]);
|
||||
},
|
||||
error: function (error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
@@ -313,7 +313,13 @@ function updateCanvas(parsedData, signalChart) {
|
||||
socket.onmessage = (event) => {
|
||||
parsedData = JSON.parse(event.data);
|
||||
updatePanels(parsedData);
|
||||
data.push(parsedData.signal);
|
||||
if(localStorage.getItem("smoothSignal") == 'true') {
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
data.push(averageSignal);
|
||||
} else {
|
||||
data.push(parsedData.signal);
|
||||
}
|
||||
};
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
@@ -365,13 +371,13 @@ function checkKey(e) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
switch (e.keyCode) {
|
||||
case 66: // Back to previous frequency
|
||||
socket.send("T" + (previousFreq * 1000));
|
||||
tuneTo(previousFreq);
|
||||
break;
|
||||
case 82: // RDS Reset (R key)
|
||||
socket.send("T" + (currentFreq.toFixed(1) * 1000));
|
||||
tuneTo(Number(currentFreq));
|
||||
break;
|
||||
case 38:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + ((currentFreq > 30) ? 10 : 1)));
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + ((currentFreq > 30) ? 10 : 1)));
|
||||
break;
|
||||
case 40:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - ((currentFreq > 30) ? 10 : 1)));
|
||||
@@ -382,6 +388,22 @@ function checkKey(e) {
|
||||
case 39:
|
||||
tuneUp();
|
||||
break;
|
||||
case 112: // F1
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset1')));
|
||||
break;
|
||||
case 113: // F2
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset2')));
|
||||
break;
|
||||
case 114: // F3
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset3')));
|
||||
break;
|
||||
case 115: // F4
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset4')));
|
||||
break;
|
||||
default:
|
||||
// Handle default case if needed
|
||||
break;
|
||||
|
||||
@@ -1,33 +1,56 @@
|
||||
$(document).ready(function() {
|
||||
// Cache jQuery objects for reuse
|
||||
var modal = $("#myModal");
|
||||
var modalPanel = $(".modal-panel");
|
||||
var chatPanel = $(".modal-panel-chat");
|
||||
var chatOpenBtn = $(".chatbutton");
|
||||
var openBtn = $("#settings");
|
||||
var closeBtn = $("#closeModal, #closeModalButton");
|
||||
var closeBtn = $(".closeModal, .closeModalButton");
|
||||
|
||||
// Function to open the modal
|
||||
function openModal() {
|
||||
modal.css("display", "block");
|
||||
modalPanel.css("display", "block");
|
||||
setTimeout(function() {
|
||||
modal.css("opacity", 1);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Function to close the modal
|
||||
function closeModal() {
|
||||
modal.css("opacity", 0);
|
||||
function openChat() {
|
||||
modal.css("display", "block");
|
||||
chatPanel.css("display", "block");
|
||||
setTimeout(function() {
|
||||
modal.css("display", "none");
|
||||
}, 300);
|
||||
modal.css("opacity", 1);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Event listeners for the open and close buttons
|
||||
openBtn.on("click", openModal);
|
||||
closeBtn.on("click", closeModal);
|
||||
|
||||
// Close the modal when clicking outside of it
|
||||
$(document).on("click", function(event) {
|
||||
if ($(event.target).is(modal)) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
// Function to close the modal
|
||||
function closeModal() {
|
||||
modal.css("opacity", 0);
|
||||
setTimeout(function() {
|
||||
modal.css("display", "none");
|
||||
modalPanel.css("display", "none");
|
||||
chatPanel.css("display", "none");
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Event listeners for the open and close buttons
|
||||
openBtn.on("click", openModal);
|
||||
chatOpenBtn.on("click", openChat);
|
||||
closeBtn.on("click", closeModal);
|
||||
|
||||
// Close the modal when clicking outside of it
|
||||
$(document).on("click", function(event) {
|
||||
if ($(event.target).is(modal)) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close the modal when pressing ESC key
|
||||
$(document).on("keydown", function(event) {
|
||||
if (event.key === "Escape") {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
var currentVersion = 'v1.1.0 [27.2.2024]';
|
||||
var currentVersion = 'v1.1.1 [1.3.2024]';
|
||||
|
||||
/**
|
||||
* Themes
|
||||
@@ -8,13 +8,13 @@
|
||||
*/
|
||||
const themes = {
|
||||
theme1: ['rgba(32, 34, 40, 1)', 'rgba(88, 219, 171, 1)', 'rgba(255, 255, 255, 1)' ], // Retro (Default)
|
||||
theme2: [ 'rgba(31, 12, 12, 1)', 'rgba(255, 112, 112, 1)', 'rgba(255, 255, 255, 1)' ], // Red
|
||||
theme3: [ 'rgba(18, 28, 12, 1)', 'rgba(169, 255, 112, 1)', 'rgba(255, 255, 255, 1)' ], // Green
|
||||
theme4: [ 'rgba(12, 28, 27, 1)', 'rgba(104, 247, 238, 1)', 'rgba(255, 255, 255, 1)' ], // Cyan
|
||||
theme5: [ 'rgba(23, 17, 6, 1)', 'rgba(245, 182, 66, 1)', 'rgba(255, 255, 255, 1)' ], // Orange
|
||||
theme6: [ 'rgba(33, 9, 29, 1)', 'rgba(237, 81, 211, 1)', 'rgba(255, 255, 255, 1)' ], // Pink
|
||||
theme2: [ 'rgba(21, 32, 33, 1)', 'rgba(203, 202, 165, 1)', 'rgba(255, 255, 255, 1)' ], // Cappuccino
|
||||
theme3: [ 'rgba(18, 18, 12, 1)', 'rgba(169, 255, 112, 1)', 'rgba(255, 255, 255, 1)' ], // Nature
|
||||
theme4: [ 'rgba(12, 28, 27, 1)', 'rgba(104, 247, 238, 1)', 'rgba(255, 255, 255, 1)' ], // Ocean
|
||||
theme5: [ 'rgba(23, 17, 6, 1)', 'rgba(245, 182, 66, 1)', 'rgba(255, 255, 255, 1)' ], // Terminal
|
||||
theme6: [ 'rgba(33, 9, 29, 1)', 'rgba(250, 82, 141, 1)', 'rgba(255, 255, 255, 1)' ], // Nightlife
|
||||
theme7: [ 'rgba(13, 11, 26, 1)', 'rgba(128, 105, 250, 1)', 'rgba(255, 255, 255, 1)' ], // Blurple
|
||||
theme8: [ 'rgba(252, 186, 3, 1)', 'rgba(0, 0, 0, 1)', 'rgba(0, 0, 0, 1)' ], // Sunny
|
||||
theme8: [ 'rgba(252, 186, 3, 1)', 'rgba(0, 0, 0, 1)', 'rgba(0, 0, 0, 1)' ], // Construction
|
||||
theme9: [ 'rgba(0, 0, 0, 1)', 'rgba(204, 204, 204, 1)', 'rgba(255, 255, 255, 1)' ], // AMOLED
|
||||
};
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ $.getScript('./js/main.js');
|
||||
$.getScript('./js/dropdown.js');
|
||||
$.getScript('./js/modal.js');
|
||||
$.getScript('./js/settings.js');
|
||||
$.getScript('./js/chat.js');
|
||||
@@ -23,6 +23,7 @@
|
||||
<li data-panel="status" class="active">Status</li>
|
||||
<li data-panel="connection">Connection</li>
|
||||
<li data-panel="audio">Audio</li>
|
||||
<li data-panel="webserver">Webserver</li>
|
||||
<li data-panel="identification">Identification</li>
|
||||
<li data-panel="mapbroadcast">Online map</li>
|
||||
<li data-panel="maintenance">Maintenance</li>
|
||||
@@ -36,23 +37,49 @@
|
||||
<h2>STATUS</h2>
|
||||
|
||||
<div class="panel-100 flex-container auto">
|
||||
<div class="panel-33 bg-color-2">
|
||||
<div class="panel-33">
|
||||
<span class="text-medium-big color-5"><%= onlineUsers %></span>
|
||||
<p>Online users</p>
|
||||
</div>
|
||||
|
||||
<div class="panel-33 bg-color-2">
|
||||
<div class="panel-33">
|
||||
<span class="text-medium-big color-5"><%= memoryUsage %></span>
|
||||
<p>Memory usage</p>
|
||||
</div>
|
||||
|
||||
<div class="panel-33 bg-color-2">
|
||||
<div class="panel-33">
|
||||
<span class="text-medium-big color-5"><%= processUptime %></span>
|
||||
<p>Uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Console output</h2>
|
||||
<h3>Current users</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Location</th>
|
||||
<th>Online since</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (connectedUsers.length > 0) { %>
|
||||
<% connectedUsers.forEach(user => { %>
|
||||
<tr>
|
||||
<td><%= user.ip %></td>
|
||||
<td><%= user.location %></td>
|
||||
<td><%= user.time %></td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center">No users online</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Console</h3>
|
||||
<% if (consoleOutput && consoleOutput.length > 0) { %>
|
||||
<div class="panel-100 br-5 p-10 text-small text-left top-10" id="console-output">
|
||||
<% consoleOutput.forEach(function(log) { %>
|
||||
@@ -153,6 +180,61 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-100 tab-content" id="webserver">
|
||||
<h2>Webserver settings</h2>
|
||||
<h3>Antenna options</h3>
|
||||
<div class="form-group checkbox bottom-20">
|
||||
<input type="checkbox" id="antenna-switch">
|
||||
<label for="antenna-switch">Enable the antenna switch</label>
|
||||
</div><br>
|
||||
|
||||
<h3>Tuning options</h3>
|
||||
<p>If you want to limit which frequencies the users can tune to, you can set the lower and upper limit here.<br>
|
||||
<span class="text-gray">Enter frequencies in MHz.</span>
|
||||
</p>
|
||||
<div class="form-group checkbox">
|
||||
<input type="checkbox" id="tuning-limit">
|
||||
<label for="tuning-limit">Limit tuning</label>
|
||||
</div><br>
|
||||
<div class="form-group">
|
||||
<label for="tuning-lower-limit">Lower limit:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="0" name="tuning-lower-limit" id="tuning-lower-limit">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tuning-upper-limit">Upper Limit:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="108" name="tuning-upper-limit" id="tuning-upper-limit">
|
||||
</div>
|
||||
|
||||
<h3>Presets</h3>
|
||||
<p>You can set up to 4 presets. These presets are accessible with the F1-F4 buttons.<br>
|
||||
<span class="text-gray">Enter frequencies in MHz.</span></p>
|
||||
<div class="form-group">
|
||||
<label for="preset1">Preset 1:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="87.5" name="preset1" id="preset1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="preset2">Preset 2:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="87.5" name="preset2" id="preset2">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="preset1">Preset 3:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="87.5" name="preset3" id="preset3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="preset1">Preset 4:</label>
|
||||
<input class="input-text w-100" type="text" placeholder="87.5" name="preset4" id="preset4">
|
||||
</div>
|
||||
|
||||
<h3>Banlist</h3>
|
||||
<p>If you have users that don't behave in your chat, you can choose to ban them by their IP address.<br>
|
||||
<span class="text-gray">You can see their IP address by hovering over their nickname. One IP per row.</span></p>
|
||||
<div class="form-group">
|
||||
<label for="preset1">Banned users:</label>
|
||||
<textarea id="ip-addresses" placeholder="123.45.67.8"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel-100 tab-content" id="identification">
|
||||
<h2>Tuner Identification info</h2>
|
||||
@@ -216,10 +298,6 @@
|
||||
<input type="checkbox" id="shutdown-tuner">
|
||||
<label for="shutdown-tuner">Auto-shutdown [XDR Only]</label>
|
||||
</div><br>
|
||||
<div class="form-group checkbox">
|
||||
<input type="checkbox" id="antenna-switch">
|
||||
<label for="antenna-switch">Enable the antenna switch</label>
|
||||
</div><br>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tune-pass">Tune password:</label>
|
||||
|
||||
Reference in New Issue
Block a user