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

new admin system, ui changes, bugfixes

This commit is contained in:
NoobishSVK
2024-02-27 23:04:26 +01:00
parent 2315f98079
commit 1fb9b99b95
21 changed files with 1038 additions and 411 deletions

View File

@@ -18,7 +18,7 @@ const MESSAGE_PREFIX = {
// Initialize an array to store logs
const logs = [];
const maxLogLines = 100;
const maxLogLines = 250;
const logDebug = (...messages) => {
if (verboseMode) {

View File

@@ -210,7 +210,7 @@ var dataToSend = {
st: false,
st_forced: false,
ps: '',
tp: false,
tp: 0,
ta: 0,
ms: -1,
pty: 0,
@@ -231,7 +231,7 @@ var dataToSend = {
},
country_name: '',
country_iso: 'UN',
users: '',
users: 0,
};
var legacyRdsPiBuffer = null;

View File

@@ -31,7 +31,7 @@ function send(request) {
}
else
{
logInfo("FM-DX Server Map update successful.");
logDebug("FM-DX Server Map update successful.");
}
}
else
@@ -66,6 +66,7 @@ function sendUpdate() {
desc: serverConfig.identification.tunerDesc,
audioChannels: serverConfig.audio.audioChannels,
audioQuality: serverConfig.audio.audioBitrate,
contact: serverConfig.identification.contact || ''
};
if (serverConfig.identification.token)

View File

@@ -11,6 +11,7 @@ const httpProxy = require('http-proxy');
const https = require('https');
const app = express();
const httpServer = http.createServer(app);
const process = require("process");
// Websocket handling
const WebSocket = require('ws');
@@ -33,7 +34,7 @@ const { logDebug, logError, logInfo, logWarn } = consoleCmd;
// Create a WebSocket proxy instance
const proxy = httpProxy.createProxyServer({
target: 'ws://localhost:'+ serverConfig.webserver.audioPort, // WebSocket httpServer's address
target: 'ws://localhost:' + (Number(serverConfig.webserver.webserverPort) + 10), // WebSocket httpServer's address
ws: true, // Enable WebSocket proxying
changeOrigin: true // Change the origin of the host header to the target URL
});
@@ -98,7 +99,6 @@ function connectToXdrd() {
const lines = receivedData.split('\n');
for (const line of lines) {
if (!authFlags.receivedPassword) {
authFlags.receivedSalt = line.trim();
authenticateWithXdrd(client, authFlags.receivedSalt, serverConfig.xdrd.xdrdPassword);
@@ -225,7 +225,6 @@ app.get('/static_data', (req, res) => {
res.json({
qthLatitude: serverConfig.identification.lat,
qthLongitude: serverConfig.identification.lon,
audioPort: serverConfig.webserver.audioPort,
streamEnabled: streamEnabled
});
});
@@ -286,8 +285,7 @@ function parseMarkdown(parsed) {
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
parsed = parsed.replace(linkRegex, '<a href="$2">$1</a>');
var breakLineRegex = /\\n/g;
parsed = parsed.replace(breakLineRegex, '<br>');
parsed = parsed.replace(/\n/g, '<br>');
return parsed;
}
@@ -307,21 +305,17 @@ function removeMarkdown(parsed) {
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
parsed = parsed.replace(linkRegex, '$1');
var breakLineRegex = /\\n/g;
parsed = parsed.replace(breakLineRegex, '');
return parsed;
}
app.get('/', (req, res) => {
if (!fs.existsSync(configName + '.json')) {
parseAudioDevice((result) => {
res.render('setup', {
res.render('wizard', {
isAdminAuthenticated: true,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
consoleOutput: consoleCmd.logs });
});;
audioDevices: result.videoDevices });
});
} else {
res.render('index', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
@@ -330,21 +324,65 @@ app.get('/', (req, res) => {
tunerDesc: parseMarkdown(serverConfig.identification.tunerDesc),
tunerDescMeta: removeMarkdown(serverConfig.identification.tunerDesc),
tunerLock: serverConfig.lockToAdmin,
publicTuner: serverConfig.publicTuner
publicTuner: serverConfig.publicTuner,
antennaSwitch: serverConfig.antennaSwitch
})
}
});
app.get('/wizard', (req, res) => {
parseAudioDevice((result) => {
res.render('wizard', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices });
});
})
app.get('/setup', (req, res) => {
parseAudioDevice((result) => {
res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
consoleOutput: consoleCmd.logs });
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = formatUptime(processUptimeInSeconds);
res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: consoleCmd.logs,
onlineUsers: dataHandler.dataToSend.users
});
});
});
app.get('/api', (req, res) => {
let data = dataHandler.dataToSend;
delete data.ps_errors;
delete data.rt0_errors;
delete data.rt1_errors;
delete data.ims;
delete data.eq;
delete data.ant;
delete data.st_forced;
delete data.previousFreq;
delete data.txInfo;
res.json(data)
});
function formatUptime(uptimeInSeconds) {
const secondsInMinute = 60;
const secondsInHour = secondsInMinute * 60;
const secondsInDay = secondsInHour * 24;
const days = Math.floor(uptimeInSeconds / secondsInDay);
const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour);
const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute);
return `${days}d ${hours}h ${minutes}m`;
}
// Route for login
app.post('/login', authenticate, (req, res) => {

View File

@@ -13,12 +13,11 @@ if (index !== -1 && index + 1 < process.argv.length) {
let serverConfig = {
webserver: {
webserverIp: "0.0.0.0",
webserverPort: "8080",
audioPort: "8081"
webserverPort: 8080
},
xdrd: {
xdrdIp: "127.0.0.1",
xdrdPort: "7373",
xdrdPort: 7373,
xdrdPassword: ""
},
audio: {

View File

@@ -5,6 +5,7 @@ const { configName, serverConfig, configUpdate, configSave } = require('../serve
function enableAudioStream() {
var ffmpegCommand;
serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort);
// Specify the command and its arguments
const command = 'ffmpeg';
const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`;
@@ -13,14 +14,14 @@ function enableAudioStream() {
// Combine all the settings for the ffmpeg command
if (process.platform === 'win32') {
// Windows
ffmpegCommand = `${flags} -f dshow -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
ffmpegCommand = `${flags} -f dshow -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
} else {
// Linux
ffmpegCommand = `${flags} -f alsa -i "${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
ffmpegCommand = `${flags} -f alsa -i "${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
}
consoleCmd.logInfo("Using audio device: " + serverConfig.audio.audioDevice);
consoleCmd.logInfo("Launching audio stream on port " + serverConfig.webserver.audioPort + ".");
consoleCmd.logInfo(`Launching audio stream on internal port ${serverConfig.webserver.webserverPort + 10}.`);
// Spawn the child process
if(serverConfig.audio.audioDevice.length > 2) {

View File

@@ -48,7 +48,7 @@ function processData(data, piCode, rdsPs) {
const city = data.locations[cityId];
if (city.stations) {
for (const station of city.stations) {
if (station.pi === piCode && !station.extra && station.ps && station.ps.toLowerCase().includes(rdsPs.replace(/ /g, '_').replace(/^_*(.*?)_*$/, '$1').toLowerCase())) {
if (station.pi === piCode.toUpperCase() && !station.extra && station.ps && station.ps.toLowerCase().includes(rdsPs.replace(/ /g, '_').replace(/^_*(.*?)_*$/, '$1').toLowerCase())) {
const distance = haversine(serverConfig.identification.lat, serverConfig.identification.lon, city.lat, city.lon);
const score = (10*Math.log10(station.erp*1000)) / distance.distanceKm; // Calculate score
if (score > maxScore) {

View File

@@ -1,13 +1,22 @@
h1 {
color: var(--color-4);
font-size: 52px;
font-weight: 300;
font-size: 32px;
font-weight: 700;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 0;
}
.modal-panel-content h1 {
text-transform: initial;
font-weight: 300;
font-size: 42px;
}
h1#tuner-name {
font-size: 32px;
font-weight: 300;
text-transform: initial;
}
h2 {
@@ -18,6 +27,7 @@ h2 {
h3 {
font-size: 22px;
color: var(--color-4);
}
h4 {
@@ -139,6 +149,9 @@ label {
.checkbox label {
position: relative;
cursor: pointer;
display: flex;
align-items: center;
user-select: none;
}
.checkbox label:before {
@@ -155,14 +168,20 @@ label {
margin-right: 5px;
}
.form-group input:checked + label:before {
background-color: var(--color-4);
}
.form-group input:checked + label:after {
content: '✓';
display: block;
position: absolute;
top: 2px;
left: 6px;
width: 16px;
height: 16px;
font-size: 18px;
top: -1px;
left: 5px;
width: 18px;
height: 18px;
color: var(--color-main);
}
.tuner-info {
@@ -170,6 +189,12 @@ label {
margin-bottom: 0px !important;
}
.settings-heading {
font-size: 32px;
padding-top: 20px;
text-transform: uppercase;
}
@media (max-width: 768px) {
canvas, #flags-container {
display: none;

View File

@@ -18,6 +18,57 @@ button:hover {
opacity: 0.6;
}
.btn-next {
width: 200px;
padding: 10px;
font-weight: bold;
color: var(--color-main);
margin: 30px 5px;
text-transform: uppercase;
}
.btn-prev {
width: 48px;
padding: 10px;
color: var(--color-main);
background-color: var(--color-3);
margin: 30px 5px;
}
.btn-rounded-cube {
width: 64px;
height: 64px;
background: var(--color-2);
color: var(--color-main);
border-radius: 30px;
margin-right: 10px;
margin-left: 10px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 24px;
font-weight: 300;
cursor: default;
}
.btn-rounded-cube:not(:first-child)::before {
content: "";
width: 20px;
height: 2px;
background: var(--color-2);
position: absolute;
right: 64px;
}
.btn-rounded-cube.activated {
background-color: var(--color-4);
}
.btn-rounded-cube.activated::before {
background-color: var(--color-4);
}
input[type="text"], textarea, input[type="password"] {
width: 300px;
min-height: 46px;
@@ -267,4 +318,4 @@ select option {
select:hover {
background: var(--color-5);
}
}

View File

@@ -14,6 +14,22 @@
background: transparent !important;
}
.bg-color-1 {
background-color: var(--color-1);
}
.bg-color-2 {
background-color: var(--color-2);
}
.bg-color-3 {
background-color: var(--color-3);
}
.bg-color-4 {
background-color: var(--color-4);
}
.color-1 {
color: var(--color-1);
}
@@ -30,6 +46,11 @@
color: var(--color-4);
}
.color-5 {
color: var(--color-5);
}
.br-0 {
border-radius: 0px;
}
@@ -126,7 +147,7 @@
}
.text-bold {
font-weight: bold;
font-weight: bold !important;
}
.text-monospace {
@@ -134,7 +155,7 @@
}
.text-gray {
color: #666;
opacity: 0.7;
}
.text-red {
@@ -177,6 +198,10 @@
cursor: pointer;
}
.hidden {
display: none;
}
@media only screen and (max-width: 960px) {
.text-medium-big {
font-size: 32px;

View File

@@ -9,6 +9,13 @@
margin-left: 5px;
}
.setup-wrapper h2 {
font-size: 32px;
font-weight: 300;
padding: 10px;
text-transform: uppercase;
}
.setup-wrapper textarea {
width: 100%;
@@ -19,6 +26,34 @@
padding-top: 10px;
}
ul.nav {
list-style-type: none;
padding: 15px 0;
background: var(--color-2);
border-radius: 30px;
}
ul.nav li {
display: inline;
padding: 15px;
cursor: pointer;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
user-select: none;
}
ul.nav li:hover {
color: var(--color-main);
background-color: var(--color-4);
}
li.active {
background-color: var(--color-3);
}
.tab-content {
display: none;
}
#map {
height:400px;
width:100%;
@@ -33,10 +68,37 @@
margin: 8px;
}
#console-output {
background-color: #111;
height: 300px;
overflow-y:auto;
}
.w-200 {
width: 200px !important
}
.w-150 {
width: 150px !important
}
.w-100 {
width: 100px !important;
}
@media only screen and (max-width: 768px) {
ul.nav {
display: flex;
overflow-y: scroll;
background: transparent;
}
ul.nav li {
background-color: var(--color-4);
color: var(--color-main);
margin: 0px 10px;
padding: 15px 35px;
border-radius: 30px;
min-width: fit-content;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -82,7 +82,7 @@
<div style="display:inline-block">
<span style="margin-left: 20px;display: block;margin-top: 2px;" class="data-flag"></span>
</div>
<span style="margin-left: 20px;" class="data-st">ST</span>
<span id="stereo-container" class="pointer"><span style="margin-left: 20px;" class="data-st">ST</span></span>
<span style="margin-left: 15px;" class="data-ms">MS</span>
</h3>
</div>
@@ -115,6 +115,8 @@
<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 Button"><i class="fa-solid fa-play"></i></button>
</div>
<% if (antennaSwitch) { %>
<div class="panel-50 no-bg h-100 m-0 dropdown" id="data-ant" style="margin-right: 25px;">
<input type="text" placeholder="Ant A" readonly />
<ul class="options">
@@ -124,6 +126,8 @@
<li data-value="3" class="option">Ant D</li>
</ul>
</div>
<% } %>
<div class="panel-100 no-bg h-100 m-0 button-eq">
<button id="data-eq" style="border-radius: 30px 0px 0px 30px;" aria-label="EQ / RF+ Filter"><span class="text-bold">EQ</span><br><span class="text-smaller">(RF+)</span></button>
</div>
@@ -211,7 +215,7 @@
<div class="dropdown" id="theme-selector">
<input type="text" placeholder="Theme" readonly />
<ul class="options">
<li class="option" data-value="theme1">Monochrome</li>
<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>
@@ -219,7 +223,7 @@
<li class="option" data-value="theme6">Pink</li>
<li class="option" data-value="theme7">Blurple</li>
<li class="option" data-value="theme8">Bee</li>
<li class="option" data-value="theme9">Retro</li>
<li class="option" data-value="theme9">AMOLED</li>
</ul>
</div>
</div>
@@ -244,6 +248,10 @@
<input type="checkbox" id="ps-underscores">
<label for="ps-underscores">Add underscores to RDS PS</label>
</div>
<div class="form-group checkbox">
<input type="checkbox" id="smooth-signal">
<label for="smooth-signal">Smooth signal</label>
</div>
<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>
@@ -273,7 +281,8 @@
<div class="version-info">
<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);">v1.0.9 [23/2/2024]</span>
<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>
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
var _3LAS_Settings = /** @class */ (function () {
function _3LAS_Settings() {
this.SocketHost = document.location.hostname ? document.location.hostname : "127.0.0.1";
this.SocketPort = localStorage.getItem('audioPort') ? localStorage.getItem('audioPort') : 8081;
this.SocketPath = "/";
this.WebRTC = new WebRTC_Settings();
this.Fallback = new Fallback_Settings();

145
web/js/confighandler.js Normal file
View File

@@ -0,0 +1,145 @@
function submitData() {
const webserverIp = $('#webserver-ip').val() || '0.0.0.0';
const webserverPort = $('#webserver-port').val() || '8080';
const xdrdIp = $('#xdrd-ip').val() || '127.0.0.1';
const xdrdPort = $('#xdrd-port').val() || '7373';
const xdrdPassword = $('#xdrd-password').val() || 'password';
const audioDevice = $('#audio-devices').val() || 'Microphone (High Definition Audio Device)';
const audioChannels = ($('.options .option').filter(function() {
return $(this).text() === $('#audio-channels').val();
}).data('value') || 2);
const audioBitrate = ($('.options .option').filter(function() {
return $(this).text() === $('#audio-quality').val();
}).data('value') || "192k");
const tunerName = $('#webserver-name').val() || 'FM Tuner';
const tunerDesc = $('#webserver-desc').val() || 'Default FM tuner description';
const broadcastTuner = $("#broadcast-tuner").is(":checked");
const contact = $("#owner-contact").val() || '';
const lat = $('#lat').val();
const lon = $('#lng').val();
const proxyIp = $("#broadcast-address").val();
const tunePass = $('#tune-pass').val();
const adminPass = $('#admin-pass').val();
const publicTuner = $("#tuner-public").is(":checked");
const lockToAdmin = $("#tuner-lock").is(":checked");
const autoShutdown = $("#shutdown-tuner").is(":checked") || false;
const antennaSwitch = $("#antenna-switch").is(":checked") || false;
const data = {
webserver: {
webserverIp,
webserverPort,
},
xdrd: {
xdrdIp,
xdrdPort,
xdrdPassword
},
audio: {
audioDevice,
audioChannels,
audioBitrate,
},
identification: {
tunerName,
tunerDesc,
broadcastTuner,
contact,
lat,
lon,
proxyIp
},
password: {
tunePass,
adminPass,
},
publicTuner,
lockToAdmin,
autoShutdown,
antennaSwitch,
};
if(adminPass.length < 1) {
alert('You need to fill in the admin password before continuing further.');
return;
}
// Send data to the server using jQuery
$.ajax({
url: './saveData',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (message) {
alert(message);
},
error: function (error) {
console.error(error);
}
});
}
function fetchData() {
// Make a GET request to retrieve the data.json file
fetch("./getData")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
$('#webserver-ip').val(data.webserver.webserverIp);
$('#webserver-port').val(data.webserver.webserverPort);
$('#xdrd-ip').val(data.xdrd.xdrdIp);
$('#xdrd-port').val(data.xdrd.xdrdPort);
$('#xdrd-password').val(data.xdrd.xdrdPassword);
$('#audio-devices').val(data.audio.audioDevice);
$('#audio-channels').val(data.audio.audioChannels);
$('#audio-quality').val(data.audio.audioBitrate);
$('#webserver-name').val(data.identification.tunerName);
$('#webserver-desc').val(data.identification.tunerDesc);
$("#broadcast-tuner").prop("checked", data.identification.broadcastTuner);
$("#broadcast-address").val(data.identification.proxyIp);
$("#owner-contact").val(data.identification.contact);
$('#lat').val(data.identification.lat);
$('#lng').val(data.identification.lon);
$('#tune-pass').val(data.password.tunePass);
$('#admin-pass').val(data.password.adminPass);
$("#tuner-public").prop("checked", data.publicTuner);
$("#tuner-lock").prop("checked", data.lockToAdmin);
$("#shutdown-tuner").prop("checked", data.autoShutdown);
$("#antenna-switch").prop("checked", data.antennaSwitch);
// Check if latitude and longitude are present in the data
if (data.identification.lat && data.identification.lon) {
// Set the map's center to the received coordinates
map.setView([data.identification.lat, data.identification.lon], 13);
// Add a pin to the map
if (typeof pin == "object") {
pin.setLatLng([data.identification.lat, data.identification.lon]);
} else {
pin = L.marker([data.identification.lat, data.identification.lon], { riseOnHover:true, draggable:true });
pin.addTo(map);
pin.on('drag',function(ev) {
$('#lat').val((ev.latlng.lat).toFixed(6));
$('#lng').val((ev.latlng.lng).toFixed(6));
});
}
}
})
.catch(error => {
console.error('Error fetching data:', error.message);
});
}

View File

@@ -3,6 +3,7 @@ url.protocol = url.protocol.replace('http', 'ws');
var socketAddress = url.href;
var socket = new WebSocket(socketAddress);
var parsedData, signalChart, previousFreq;
var signalData = [];
var data = [];
let updateCounter = 0;
@@ -118,6 +119,7 @@ $(document).ready(function () {
var piCodeContainer = $('#pi-code-container')[0];
var freqContainer = $('#freq-container')[0];
var txContainer = $('#data-station-container')[0];
var stereoContainer = $('#stereo-container')[0];
$("#data-eq").click(function () {
toggleButtonState("eq");
@@ -133,6 +135,7 @@ $(document).ready(function () {
$(rtContainer).on("click", copyRt);
$(txContainer).on("click", copyTx);
$(piCodeContainer).on("click", findOnMaps);
$(stereoContainer).on("click", toggleForcedStereo);
$(freqContainer).on("click", function () {
textInput.focus();
});
@@ -514,24 +517,31 @@ function findOnMaps() {
window.open(url, "_blank");
}
function updateSignalUnits(parsedData) {
function updateSignalUnits(parsedData, averageSignal) {
const signalUnit = localStorage.getItem('signalUnit');
let currentSignal;
if(localStorage.getItem("smoothSignal") == 'true') {
currentSignal = averageSignal
} else {
currentSignal = parsedData.signal;
}
let signalText = $('#signal-units');
let signalValue;
switch (signalUnit) {
case 'dbuv':
signalValue = parsedData.signal - 11.25;
signalValue = currentSignal - 11.25;
signalText.text('dBµV');
break;
case 'dbm':
signalValue = parsedData.signal - 120;
signalValue = currentSignal - 120;
signalText.text('dBm');
break;
default:
signalValue = parsedData.signal;
signalValue = currentSignal;
signalText.text('dBf');
break;
}
@@ -551,15 +561,6 @@ function updateDataElements(parsedData) {
parsedData.ps = parsedData.ps.replace(/\s/g, '_');
}
$('#data-ps').html(parsedData.ps === '?' ? "<span class='opacity-half'>?</span>" : processString(parsedData.ps, parsedData.ps_errors));
$('.data-tp').html(parsedData.tp === 0 ? "<span class='opacity-half'>TP</span>" : "TP");
$('.data-ta').html(parsedData.ta === 0 ? "<span class='opacity-half'>TA</span>" : "TA");
$('.data-ms').html(parsedData.ms === 0
? "<span class='opacity-half'>M</span><span class='opacity-full'>S</span>"
: (parsedData.ms === -1
? "<span class='opacity-half'>M</span><span class='opacity-half'>S</span>"
: "<span class='opacity-full'>M</span><span class='opacity-half'>S</span>"
)
);
$('.data-pty').html(europe_programmes[parsedData.pty]);
@@ -580,6 +581,7 @@ function updateDataElements(parsedData) {
$('#data-rt0').html(processString(parsedData.rt0, parsedData.rt0_errors));
$('#data-rt1').html(processString(parsedData.rt1, parsedData.rt1_errors));
$('.data-flag').html(`<i title="${parsedData.country_name}" class="flag-sm flag-sm-${parsedData.country_iso}"></i>`);
$('.data-flag-big').html(`<i title="${parsedData.country_name}" class="flag-md flag-md-${parsedData.country_iso}"></i>`);
$('#data-ant input').val($('#data-ant li[data-value="' + parsedData.ant + '"]').text());
if (parsedData.txInfo.station.length > 1) {
@@ -596,6 +598,18 @@ function updateDataElements(parsedData) {
}
updateCounter++;
if(updateCounter % 8 === 0) {
$('.data-tp').html(parsedData.tp === 0 ? "<span class='opacity-half'>TP</span>" : "TP");
$('.data-ta').html(parsedData.ta === 0 ? "<span class='opacity-half'>TA</span>" : "TA");
$('.data-ms').html(parsedData.ms === 0
? "<span class='opacity-half'>M</span><span class='opacity-full'>S</span>"
: (parsedData.ms === -1
? "<span class='opacity-half'>M</span><span class='opacity-half'>S</span>"
: "<span class='opacity-full'>M</span><span class='opacity-half'>S</span>"
)
);
}
if (updateCounter % 30 === 0) {
$('#data-ps').attr('aria-label', parsedData.ps);
$('#data-rt0').attr('aria-label', parsedData.rt0);
@@ -608,6 +622,13 @@ let isEventListenerAdded = false;
function updatePanels(parsedData) {
updateCounter++;
signalData.push(parsedData.signal);
if (signalData.length > 8) {
signalData.shift(); // Remove the oldest element
}
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
const averageSignal = sum / signalData.length;
const sortedAf = parsedData.af.sort(compareNumbers);
const scaledArray = sortedAf.map(element => element / 1000);
@@ -642,9 +663,8 @@ function updatePanels(parsedData) {
listContainer.scrollTop(scrollTop);
}
// Update other elements every time
updateDataElements(parsedData);
updateSignalUnits(parsedData);
updateSignalUnits(parsedData, averageSignal);
$('.users-online').text(parsedData.users);
}
@@ -669,3 +689,9 @@ function toggleButtonState(buttonId) {
message += parsedData.ims ? "1" : "0";
socket.send(message);
}
function toggleForcedStereo() {
var message = "B";
message += parsedData.st_forced = (parsedData.st_forced == "1") ? "0" : "1";
socket.send(message);
}

View File

@@ -1,11 +1,13 @@
var currentVersion = 'v1.1.0 [29.2.2024]';
/**
* Themes
* @param first color
* @param second color
* @param text color
*/
* Themes
* @param first color
* @param second color
* @param text color
*/
const themes = {
theme1: [ 'rgba(0, 0, 0, 1)', 'rgba(204, 204, 204, 1)', 'rgba(255, 255, 255, 1)' ], // Monochrome (Default)
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
@@ -13,143 +15,154 @@
theme6: [ 'rgba(33, 9, 29, 1)', 'rgba(237, 81, 211, 1)', 'rgba(255, 255, 255, 1)' ], // Pink
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
theme9: ['rgba(32, 34, 40, 1)', 'rgba(88, 219, 171, 1)', 'rgba(255, 255, 255, 1)' ] // Retro
};
// Signal Units
const signalUnits = {
dbf: ['dBf'],
dbuv: ['dBµV'],
dbm: ['dBm'],
};
$(document).ready(() => {
// Theme Selector
const themeSelector = $('#theme-selector');
const savedTheme = localStorage.getItem('theme');
const savedUnit = localStorage.getItem('signalUnit');
if (savedTheme && themes[savedTheme]) {
setTheme(savedTheme);
themeSelector.find('input').val(themeSelector.find('.option[data-value="' + savedTheme + '"]').text());
}
themeSelector.on('click', '.option', (event) => {
const selectedTheme = $(event.target).data('value');
setTheme(selectedTheme);
themeSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
localStorage.setItem('theme', selectedTheme);
});
// Signal Selector
const signalSelector = $('#signal-selector');
if (localStorage.getItem('signalUnit')) {
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
}
signalSelector.on('click', '.option', (event) => {
const selectedSignalUnit = $(event.target).data('value');
signalSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
localStorage.setItem('signalUnit', selectedSignalUnit);
});
$('#login-form').submit(function (event) {
event.preventDefault();
theme9: [ 'rgba(0, 0, 0, 1)', 'rgba(204, 204, 204, 1)', 'rgba(255, 255, 255, 1)' ], // AMOLED
};
// Perform an AJAX request to the /login endpoint
$.ajax({
type: 'POST',
url: './login',
data: $(this).serialize(),
success: function (data) {
// Update the content on the page with the message from the response
$('#login-message').text(data.message);
setTimeout(function () {
location.reload(true);
}, 1750);
},
error: function (xhr, status, error) {
// Handle error response
if (xhr.status === 403) {
// Update the content on the page with the message from the error response
$('#login-message').text(xhr.responseJSON.message);
} else {
// Handle other types of errors if needed
console.error('Error:', status, error);
}
}
});
});
// Assuming you have an anchor tag with id 'logout-link'
$('.logout-link').click(function (event) {
event.preventDefault();
// Signal Units
const signalUnits = {
dbf: ['dBf'],
dbuv: ['dBµV'],
dbm: ['dBm'],
};
$(document).ready(() => {
// Theme Selector
const themeSelector = $('#theme-selector');
const savedTheme = localStorage.getItem('theme');
const savedUnit = localStorage.getItem('signalUnit');
// Perform an AJAX request to the /logout endpoint
$.ajax({
type: 'GET', // Assuming the logout is a GET request, adjust accordingly
url: './logout',
success: function (data) {
// Update the content on the page with the message from the response
$('#login-message').text(data.message);
setTimeout(function () {
location.reload(true);
}, 1750);
},
error: function (xhr, status, error) {
// Handle error response
if (xhr.status === 403) {
// Update the content on the page with the message from the error response
$('#login-message').text(xhr.responseJSON.message);
} else {
// Handle other types of errors if needed
console.error('Error:', status, error);
}
}
});
});
var extendedFreqRange = localStorage.getItem("extendedFreqRange");
if (extendedFreqRange === "true") {
$("#extended-frequency-range").prop("checked", true);
}
// Save the value of the checkbox into local storage when its state changes
$("#extended-frequency-range").change(function() {
var isChecked = $(this).is(":checked");
localStorage.setItem("extendedFreqRange", isChecked);
});
var extendedFreqRange = localStorage.getItem("psUnderscores");
if (extendedFreqRange === "true") {
$("#ps-underscores").prop("checked", true);
}
// Save the value of the checkbox into local storage when its state changes
$("#ps-underscores").change(function() {
var isChecked = $(this).is(":checked");
localStorage.setItem("psUnderscores", isChecked);
});
});
function setTheme(themeName) {
const themeColors = themes[themeName];
if (themeColors) {
// Extracting the RGBA components and opacity value
const rgbaComponents = themeColors[2].match(/(\d+(\.\d+)?)/g);
const opacity = parseFloat(rgbaComponents[3]);
// Calculating 80% of the opacity
const newOpacity = opacity * 0.75;
// Constructing the new RGBA string with the adjusted opacity
const textColor2 = `rgba(${rgbaComponents[0]}, ${rgbaComponents[1]}, ${rgbaComponents[2]}, ${newOpacity})`;
if (savedTheme && themes[savedTheme]) {
setTheme(savedTheme);
themeSelector.find('input').val(themeSelector.find('.option[data-value="' + savedTheme + '"]').text());
}
$(':root').css('--color-main', themeColors[0]);
$(':root').css('--color-main-bright', themeColors[1]);
$(':root').css('--color-text', themeColors[2]);
$(':root').css('--color-text-2', textColor2);
}
}
themeSelector.on('click', '.option', (event) => {
const selectedTheme = $(event.target).data('value');
setTheme(selectedTheme);
themeSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
localStorage.setItem('theme', selectedTheme);
});
// Signal Selector
const signalSelector = $('#signal-selector');
if (localStorage.getItem('signalUnit')) {
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
}
signalSelector.on('click', '.option', (event) => {
const selectedSignalUnit = $(event.target).data('value');
signalSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
localStorage.setItem('signalUnit', selectedSignalUnit);
});
$('#login-form').submit(function (event) {
event.preventDefault();
// Perform an AJAX request to the /login endpoint
$.ajax({
type: 'POST',
url: './login',
data: $(this).serialize(),
success: function (data) {
// Update the content on the page with the message from the response
$('#login-message').text(data.message);
setTimeout(function () {
location.reload(true);
}, 1750);
},
error: function (xhr, status, error) {
// Handle error response
if (xhr.status === 403) {
// Update the content on the page with the message from the error response
$('#login-message').text(xhr.responseJSON.message);
} else {
// Handle other types of errors if needed
console.error('Error:', status, error);
}
}
});
});
// Assuming you have an anchor tag with id 'logout-link'
$('.logout-link').click(function (event) {
event.preventDefault();
// Perform an AJAX request to the /logout endpoint
$.ajax({
type: 'GET', // Assuming the logout is a GET request, adjust accordingly
url: './logout',
success: function (data) {
// Update the content on the page with the message from the response
$('#login-message').text(data.message);
setTimeout(function () {
location.reload(true);
}, 1750);
},
error: function (xhr, status, error) {
// Handle error response
if (xhr.status === 403) {
// Update the content on the page with the message from the error response
$('#login-message').text(xhr.responseJSON.message);
} else {
// Handle other types of errors if needed
console.error('Error:', status, error);
}
}
});
});
var extendedFreqRange = localStorage.getItem("extendedFreqRange");
if (extendedFreqRange === "true") {
$("#extended-frequency-range").prop("checked", true);
}
// Save the value of the checkbox into local storage when its state changes
$("#extended-frequency-range").change(function() {
var isChecked = $(this).is(":checked");
localStorage.setItem("extendedFreqRange", isChecked);
});
var extendedFreqRange = localStorage.getItem("psUnderscores");
if (extendedFreqRange === "true") {
$("#ps-underscores").prop("checked", true);
}
var smoothSignal = localStorage.getItem("smoothSignal");
if (smoothSignal === "true") {
$("#smooth-signal").prop("checked", true);
}
// Save the value of the checkbox into local storage when its state changes
$("#ps-underscores").change(function() {
var isChecked = $(this).is(":checked");
localStorage.setItem("psUnderscores", isChecked);
});
$("#smooth-signal").change(function() {
var isChecked = $(this).is(":checked");
localStorage.setItem("smoothSignal", isChecked);
});
$('.version-string').text(currentVersion);
});
function setTheme(themeName) {
const themeColors = themes[themeName];
if (themeColors) {
// Extracting the RGBA components and opacity value
const rgbaComponents = themeColors[2].match(/(\d+(\.\d+)?)/g);
const opacity = parseFloat(rgbaComponents[3]);
// Calculating 80% of the opacity
const newOpacity = opacity * 0.75;
// Constructing the new RGBA string with the adjusted opacity
const textColor2 = `rgba(${rgbaComponents[0]}, ${rgbaComponents[1]}, ${rgbaComponents[2]}, ${newOpacity})`;
$(':root').css('--color-main', themeColors[0]);
$(':root').css('--color-main-bright', themeColors[1]);
$(':root').css('--color-text', themeColors[2]);
$(':root').css('--color-text-2', textColor2);
}
}

View File

@@ -9,7 +9,12 @@ $(document).ready(function() {
MapCreate();
fetchData();
setTimeout( function() {
if ($('.nav li.active[data-panel="status"]').length > 0) {
$('#submit-config').hide();
}
}, 50 )
map.on('click', function(ev) {
$('#lat').val((ev.latlng.lat).toFixed(6));
$('#lng').val((ev.latlng.lng).toFixed(6));
@@ -25,7 +30,39 @@ $(document).ready(function() {
});
}
});
$('#status').show();
showPanelFromHash();
$('.nav li').click(function() {
// Remove background color from all li elements
$('.nav li').removeClass('active');
// Add background color to the clicked li element
$(this).addClass('active');
// Get the data-panel attribute value
var panelId = $(this).data('panel');
window.location.hash = panelId;
// Hide all panels
$('.tab-content').hide();
// Show the corresponding panel
$('#' + panelId).show();
if(panelId == 'identification') {
setTimeout(function () {
map.invalidateSize();
}, 200);
}
if(panelId == 'status') {
$('#submit-config').hide();
} else {
$('#submit-config').show();
}
});
$('#login-form').submit(function (event) {
event.preventDefault();
@@ -105,7 +142,9 @@ $(document).ready(function() {
});
});
$("#console-output").scrollTop($("#console-output")[0].scrollHeight);
if($("#console-output").length > 0) {
$("#console-output").scrollTop($("#console-output")[0].scrollHeight);
}
});
function MapCreate() {
@@ -126,149 +165,19 @@ function MapCreate() {
}).addTo(map);
}
function fetchData() {
// Make a GET request to retrieve the data.json file
fetch("./getData")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
$('#webserver-ip').val(data.webserver.webserverIp);
$('#webserver-port').val(data.webserver.webserverPort);
$('#audio-port').val(data.webserver.audioPort);
$('#xdrd-ip').val(data.xdrd.xdrdIp);
$('#xdrd-port').val(data.xdrd.xdrdPort);
$('#xdrd-password').val(data.xdrd.xdrdPassword);
$('#audio-devices').val(data.audio.audioDevice);
$('#audio-channels').val(data.audio.audioChannels);
$('#audio-quality').val(data.audio.audioBitrate);
$('#webserver-name').val(data.identification.tunerName);
$('#webserver-desc').val(data.identification.tunerDesc);
$('#lat').val(data.identification.lat);
$('#lng').val(data.identification.lon);
$("#broadcast-tuner").prop("checked", data.identification.broadcastTuner);
$("#broadcast-address").val(data.identification.proxyIp);
$('#tune-pass').val(data.password.tunePass);
$('#admin-pass').val(data.password.adminPass);
$("#tuner-public").prop("checked", data.publicTuner);
$("#tuner-lock").prop("checked", data.lockToAdmin);
$("#shutdown-tuner").prop("checked", data.autoShutdown);
// Check if latitude and longitude are present in the data
if (data.identification.lat && data.identification.lon) {
// Set the map's center to the received coordinates
map.setView([data.identification.lat, data.identification.lon], 13);
// Add a pin to the map
if (typeof pin == "object") {
pin.setLatLng([data.identification.lat, data.identification.lon]);
} else {
pin = L.marker([data.identification.lat, data.identification.lon], { riseOnHover:true, draggable:true });
pin.addTo(map);
pin.on('drag',function(ev) {
$('#lat').val((ev.latlng.lat).toFixed(6));
$('#lng').val((ev.latlng.lng).toFixed(6));
});
}
}
})
.catch(error => {
console.error('Error fetching data:', error.message);
});
}
function submitData() {
const webserverIp = $('#webserver-ip').val() || '0.0.0.0';
const webserverPort = $('#webserver-port').val() || '8080';
const audioPort = $('#audio-port').val() || '8081';
const xdrdIp = $('#xdrd-ip').val() || '127.0.0.1';
const xdrdPort = $('#xdrd-port').val() || '7373';
const xdrdPassword = $('#xdrd-password').val() || 'password';
const audioDevice = $('#audio-devices').val() || 'Microphone (High Definition Audio Device)';
const audioChannels = ($('.options .option').filter(function() {
return $(this).text() === $('#audio-channels').val();
}).data('value') || 2);
const audioBitrate = ($('.options .option').filter(function() {
return $(this).text() === $('#audio-quality').val();
}).data('value') || "192k");
const tunerName = $('#webserver-name').val() || 'FM Tuner';
const tunerDesc = $('#webserver-desc').val() || 'Default FM tuner description';
const lat = $('#lat').val();
const lon = $('#lng').val();
const broadcastTuner = $("#broadcast-tuner").is(":checked");
const proxyIp = $("#broadcast-address").val();
const tunePass = $('#tune-pass').val();
const adminPass = $('#admin-pass').val();
const publicTuner = $("#tuner-public").is(":checked");
const lockToAdmin = $("#tuner-lock").is(":checked");
const autoShutdown = $("#shutdown-tuner").is(":checked");
const data = {
webserver: {
webserverIp,
webserverPort,
audioPort
},
xdrd: {
xdrdIp,
xdrdPort,
xdrdPassword
},
audio: {
audioDevice,
audioChannels,
audioBitrate,
},
identification: {
tunerName,
tunerDesc,
lat,
lon,
broadcastTuner,
proxyIp
},
password: {
tunePass,
adminPass,
},
publicTuner,
lockToAdmin,
autoShutdown
};
if(adminPass.length < 1) {
alert('You need to fill in the admin password before continuing further.');
return;
function showPanelFromHash() {
var panelId = window.location.hash.substring(1);
if (panelId) {
// Hide all panels
$('.tab-content').hide();
// Show the panel corresponding to the hash fragment
$('#' + panelId).show();
// Remove active class from all li elements
$('.nav li').removeClass('active');
// Add active class to the corresponding li element
$('.nav li[data-panel="' + panelId + '"]').addClass('active');
}
// Send data to the server using jQuery
$.ajax({
url: './saveData',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (message) {
alert(message);
},
error: function (error) {
console.error(error);
}
});
}
}

62
web/js/wizard.js Normal file
View File

@@ -0,0 +1,62 @@
$(document).ready(function() {
if($('.step:visible').index() == 0) {
$('.btn-prev').hide();
}
$('.btn-next').click(function() {
var currentStep = $('.step:visible');
var nextStep = currentStep.next('.step');
if (nextStep.length !== 0) {
currentStep.hide();
nextStep.show();
updateProgressBar(nextStep);
} else {
submitData();
}
updateWizardContent();
});
$('.btn-prev').click(function() {
var currentStep = $('.step:visible');
var nextStep = currentStep.prev('.step');
if (nextStep.length !== 0) {
currentStep.hide();
nextStep.show();
updateProgressBar(nextStep);
} else {
alert('You have reached the beginning of the wizard.');
}
updateWizardContent();
});
});
// Function to update the progress bar buttons
function updateProgressBar(currentStep) {
var stepIndex = $('.step').index(currentStep) + 1;
$('.btn-rounded-cube').removeClass('activated');
$('.btn-rounded-cube:lt(' + stepIndex + ')').addClass('activated');
}
function updateWizardContent() {
if($('.step:visible').index() == 0) {
$('.btn-prev').hide();
} else {
$('.btn-prev').show();
}
if($('.step:visible').index() == 2) {
setTimeout(function () {
map.invalidateSize();
}, 200);
}
if($('.step:visible').index() == 3) {
$('.btn-next').text('Save');
} else {
$('.btn-next').text('Next')
}
}

View File

@@ -15,15 +15,63 @@
<div id="wrapper" class="setup-wrapper">
<% if (isAdminAuthenticated) { %>
<div class="panel-100 no-bg">
<h1>FM-DX WebServer</h1>
<img class="top-10" src="./images/openradio_logo_neutral.png" height="64px">
<h2 class="text-monospace text-light">[ADMIN PANEL]</h2>
<p>This web setup allows you to set up your entire tuner. <br>Some settings will only change after a server restart.</p>
<p>In case you are setting up the webserver for the first time, we already filled fail-safe defaults for you.</p>
</div>
<div class="flex-container">
<div class="panel-50" style="min-height: 120px;margin-bottom: 0;">
<h2>BASIC SETTINGS</h2>
<h3>Connection to xdrd:</h3>
<div class="panel-100">
<ul class="nav">
<li data-panel="status" class="active">Status</li>
<li data-panel="connection">Connection</li>
<li data-panel="audio">Audio</li>
<li data-panel="identification">Identification</li>
<li data-panel="mapbroadcast">Online map</li>
<li data-panel="maintenance">Maintenance</li>
<li class="logout-link text-gray"><i class="fas fa-sign-out"></i></li>
</ul>
</div>
<div id="login-message"></div>
<div class="panel-100">
<div class="panel-100 tab-content" id="status">
<h2>STATUS</h2>
<div class="panel-100 flex-container auto">
<div class="panel-33 bg-color-2">
<span class="text-medium-big color-5"><%= onlineUsers %></span>
<p>Online users</p>
</div>
<div class="panel-33 bg-color-2">
<span class="text-medium-big color-5"><%= memoryUsage %></span>
<p>Memory usage</p>
</div>
<div class="panel-33 bg-color-2">
<span class="text-medium-big color-5"><%= processUptime %></span>
<p>Uptime</p>
</div>
</div>
<h2>Console output</h2>
<% 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) { %>
<pre class="m-0" style="white-space:pre-wrap;"><%= log %></pre>
<% }); %>
</div>
<% } else { %>
<p>No console output available.</p>
<% } %>
<p>Version: <span class="version-string color-4"></span></p>
<p class="p-bottom-20"><a href="https://github.com/noobishsvk/fm-dx-webserver" target="_blank">Check for the latest source code</a> • <a href="https://buymeacoffee.com/noobish" target="_blank">Support the developer</a></p>
</div>
<div class="panel-100 tab-content" id="connection">
<h2>Connection settings</h2>
<p>You can set up your connection settings here. Changing these settings requires a server restart.</p>
<h3>Tuner connection:</h3>
<p class="text-gray">If you are connecting your tuner <strong>wirelessly</strong>, enter the tuner IP. <br> If you use <strong>xdrd</strong>, use 127.0.0.1 as your IP.</p>
<div class="form-group">
<label for="xdrd-ip">xdrd ip address:</label>
<input class="input-text w-150" type="text" name="xdrd-ip" id="xdrd-ip" placeholder="127.0.0.1">
@@ -38,6 +86,7 @@
</div>
<br>
<h3>Webserver connection:</h3>
<p class="text-gray">Leave the IP at 0.0.0.0 unless you explicitly know you have to change it.<br>Don't enter your public IP here.</p>
<div class="form-group">
<label for="webserver-ip">Webserver IP:</label>
<input class="input-text w-150" type="text" name="webserver-ip" id="webserver-ip" placeholder="0.0.0.0">
@@ -46,17 +95,18 @@
<label for="webserver-port">Webserver port:</label>
<input class="input-text w-100" type="text" name="webserver-port" id="webserver-port" placeholder="8080">
</div>
<div class="form-group">
<label for="audio-port">Audio port:</label>
<input class="input-text w-150" type="text" name="audio-port" id="audio-port" placeholder="8081">
</div>
<br>
</div>
<div class="panel-50" style="min-height: 120px;margin-bottom: 0;">
<h2>AUDIO SETTINGS</h2>
<div class="panel-100 tab-content" id="audio">
<h2>Audio settings</h2>
<p>You can set up your audio settings here. Changing these settings requires a server restart.</p>
<div class="panel-100 p-bottom-20">
<div class="form-group">
<p class="text-left">Your audio device port.<br>
<span class="text-gray">This is where your tuner is plugged in.</span>
</p>
<label for="audio-devices"><i class="fa-solid fa-headphones"></i> STREAM AUDIO FROM:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-devices" type="text" name="audio-devices" placeholder="Choose your audio device" readonly />
@@ -72,6 +122,9 @@
</div>
<div class="form-group">
<p class="text-left">Audio channel count.<br>
<span class="text-gray">1: Mono • 2: Stereo</span>
</p>
<label for="audio-devices"><i class="fa-solid fa-microphone-lines"></i> AUDIO CHANNELS:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-channels" type="text" name="audio-channels" placeholder="Stereo" readonly />
@@ -83,6 +136,9 @@
</div>
<div class="form-group">
<p class="text-left">The bitrate of the mp3 audio.<br>
<span class="text-gray">Minimum: 64 Kbps • Maximum: 256 Kbps</span>
</p>
<label for="audio-quality"><i class="fa-solid fa-wave-square"></i> AUDIO QUALITY:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-quality" type="text" name="audio-quality" placeholder="128k (standard)" readonly />
@@ -97,35 +153,21 @@
</div>
</div>
</div>
</div>
<div class="flex-container bottom-20">
<div class="panel-100" style="padding-bottom: 20px;">
<h2>TUNER IDENTIFICATION INFO</h2>
<div class="panel-100 tab-content" id="identification">
<h2>Tuner Identification info</h2>
<p class="text-gray">Set your tuner name and description here. This info will be visible to anyone who tunes in. </p>
<div class="panel-100" style="padding-left: 20px; padding-right: 20px;">
<label for="webserver-name" style="width: 100%;max-width: 768px; margin:auto;">Webserver name:</label>
<input style="width: 100%; max-width: 768px;" class="input-text" type="text" name="webserver-name" id="webserver-name" placeholder="Fill your server name here.">
<input style="width: 100%; max-width: 768px;" class="input-text" type="text" name="webserver-name" id="webserver-name" placeholder="Fill your server name here." maxlength="32">
<br>
<label for="webserver-desc" style="width: 100%;max-width: 768px; margin: auto;">Webserver description:</label>
<textarea id="webserver-desc" name="webserver-desc" placeholder="Fill the server description here. You can put useful info here such as your antenna setup. You can use simple markdown."></textarea>
</div>
<h3>Map broadcast:</h3>
<p class="m-0">If your tuner is set to public and ID information is filled, you can add your tuner to a public list.</p>
<p class="m-0">The list is available at <strong><a href="https://list.fmdx.pl" target="_blank" class="color-4">list.fmdx.pl</a></strong>.</p>
<p></p>
<div class="form-group checkbox">
<input type="checkbox" id="broadcast-tuner">
<label for="broadcast-tuner">Broadcast to map</label>
</div><br>
<div class="form-group">
<label for="broadcast-address">Broadcast address (if using a proxy):</label>
<input class="input-text" type="text" name="broadcast-address" id="broadcast-address">
<textarea id="webserver-desc" name="webserver-desc" placeholder="Fill the server description here. You can put useful info here such as your antenna setup. You can use simple markdown." maxlength="255"></textarea>
</div>
<h3>Tuner location:</h3>
<h3>Location:</h3>
<p class="text-gray">Location info is useful for automatic identification of stations using RDS.</p>
<div class="form-group">
<label for="lat">Latitude:</label>
<input class="input-text" type="text" name="lat" id="lat">
@@ -137,49 +179,61 @@
</div>
<div id="map"></div>
<br>
</div>
<div class="panel-33">
<h2>MAINTENANCE</h2>
<div class="text-left top-25 bottom-20" style="padding-left: 40px;">
<div class="panel-100 tab-content" id="mapbroadcast">
<h2>Map broadcast</h2>
<p class="m-0">If your location information is filled, you can add your tuner to a public list.</p>
<p></p>
<div class="form-group checkbox">
<input type="checkbox" id="broadcast-tuner">
<label for="broadcast-tuner">Broadcast to map</label>
</div><br>
<div class="form-group">
<label for="owner-contact">Owner contact:</label>
<input class="input-text" type="text" placeholder="Your e-mail, discord..." name="owner-contact" id="owner-contact">
<label for="broadcast-address">Broadcast address (if using a proxy):</label>
<input class="input-text" type="text" name="broadcast-address" id="broadcast-address">
</div>
<p>Check your tuner at <strong><a href="https://list.fmdx.pl" target="_blank" class="color-4">list.fmdx.pl</a></strong>.</p>
</div>
<div class="panel-100 tab-content" id="maintenance">
<h2>Maintenance</h2>
<div class="text-left top-25 bottom-20 panel-33 auto">
<div class="form-group checkbox">
<input type="checkbox" id="tuner-public">
<label for="tuner-public">Public tuner</label>
<label for="tuner-public">Allow tuner control without password</label>
</div><br>
<div class="form-group checkbox">
<input type="checkbox" id="tuner-lock">
<label for="tuner-lock">Lock to admin</label>
</div><br>
<label for="tuner-lock">Lock the tuner [only admins can tune with this option]</label>
</div><br><br>
<div class="form-group checkbox">
<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>
<input class="input-text w-150" type="password" name="tune-pass" id="tune-pass">
<input class="input-text w-200" type="password" name="tune-pass" id="tune-pass">
</div>
<div class="form-group" style="margin-bottom: 40px;">
<div class="form-group">
<label for="admin-pass">Admin setup password:</label>
<input class="input-text w-150" type="password" name="admin-pass" id="admin-pass">
<input class="input-text w-200" type="password" name="admin-pass" id="admin-pass">
</div><br>
<button style="height:48px; width: 200px;margin-bottom:20px;" onclick="submitData();">Save settings</button>
<button style="height: 48px; width: 200px;background:var(--color-3)" class="logout-link">Logout</button>
<div id="login-message"></div>
</div>
<button id="submit-config" style="height:48px; width: 200px;margin:20px;" onclick="submitData();">Save settings</button>
</div>
<div class="panel-100 p-bottom-20">
<h2>CONSOLE OUTPUT</h2>
<% if (consoleOutput && consoleOutput.length > 0) { %>
<div class="panel-100 br-5 p-10 text-small text-left top-10" id="console-output" style="background-color: var(--color-main);height: 300px;overflow-y:auto;">
<% consoleOutput.forEach(function(log) { %>
<pre class="m-0" style="white-space:pre-wrap;"><%= log %></pre>
<% }); %>
</div>
<% } else { %>
<p>No console output available.</p>
<% } %>
</div>
<div class="panel-100 no-bg">
<p>Feel free to contact us on <a href="https://discord.gg/ZAVNdS74mC" target="_blank"><strong><i class="fa-brands fa-discord"></i> Discord</strong></a> for community support.</p>
</div>
@@ -187,8 +241,7 @@
<button onclick="document.location.href='./'" id="back-btn" aria-label="Go back to tuning"><i class="fa-solid fa-arrow-left"></i></button>
<% } else { %>
<div class="panel-100 no-bg">
<img src="../favicon2.png">
<h1>FM-DX WebServer</h1>
<img class="top-10" src="./images/openradio_logo_neutral.png" height="64px">
<h2 class="text-monospace text-light">[ADMIN PANEL]</h2>
<p>You are currently not logged in as an administrator and therefore can't change the settings.</p>
<p>Please login below.</p>
@@ -206,5 +259,6 @@
<script src="js/settings.js"></script>
<script src="js/dropdown.js"></script>
<script src="js/setup.js"></script>
<script src="js/confighandler.js"></script>
</body>
</html>

208
web/wizard.ejs Normal file
View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html>
<head>
<title>FM-DX Webserver</title>
<link href="css/entry.css" type="text/css" rel="stylesheet">
<link href="css/flags.min.css" type="text/css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<link rel="icon" type="image/png" href="favicon2.png" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="wrapper" class="setup-wrapper">
<% if (isAdminAuthenticated) { %>
<div class="panel-100 no-bg">
<img class="top-10" src="../images/openradio_logo_neutral.png" height="64px">
<h1 class="top-10">FM-DX WebServer</h1>
<h2 class="text-monospace text-light text-gray">[SETUP WIZARD]</h2>
</div>
<div class="panel-100 no-bg flex-container flex-center flex-phone">
<div class="btn-rounded-cube activated">1</div>
<div class="btn-rounded-cube">2</div>
<div class="btn-rounded-cube">3</div>
<div class="btn-rounded-cube">4</div>
</div>
<div class="panel-100">
<!-- BASIC SETTINGS -->
<div class="panel-100 step" id="step1">
<h2 class="settings-heading">BASIC SETTINGS</h2>
<p class="m-0">Welcome to the setup wizard!<br> Let's set up some basic things.</p>
<h3>Tuner connection:</h3>
<p class="m-0 text-gray">If you are connecting your tuner <strong>wirelessly</strong>, enter the tuner IP. <br> If you use <strong>xdrd</strong>, use 127.0.0.1 as your IP.</p>
<div class="flex-center top-25">
<div class="form-group">
<label for="xdrd-ip">xdrd ip address:</label>
<input class="input-text w-150" type="text" name="xdrd-ip" id="xdrd-ip" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label for="xdrd-port">xdrd port:</label>
<input class="input-text w-100" type="text" name="xdrd-port" id="xdrd-port" placeholder="7373">
</div>
<div class="form-group">
<label for="xdrd-password">xdrd server password:</label>
<input class="input-text w-150" type="password" name="xdrd-password" id="xdrd-password">
</div>
</div>
<h3>Webserver connection:</h3>
<p class="m-0 text-gray">Leave the IP at 0.0.0.0 unless you explicitly know you have to change it.<br>Don't enter your public IP here.</p>
<div class="flex-center top-25">
<div class="form-group">
<label for="webserver-ip">Webserver IP:</label>
<input class="input-text w-150" type="text" name="webserver-ip" id="webserver-ip" placeholder="0.0.0.0">
</div>
<div class="form-group">
<label for="webserver-port">Webserver port:</label>
<input class="input-text w-100" type="text" name="webserver-port" id="webserver-port" placeholder="8080">
</div>
</div>
</div>
<!-- BASIC SETTINGS END -->
<!-- AUDIO SETTINGS -->
<div id="step2" class="step" style="display: none;">
<div class="panel-100" style="min-height: 120px;margin-bottom: 0;">
<h2 class="settings-heading">AUDIO SETTINGS</h2>
<p class="m-0 text-gray">In this section, we will set up the audio.<br>
Choose the audio port your tuner is connected to and desired audio settings here.</p>
<p class="text-gray">Recommended defaults have already been set for the audio quality, you can keep them as-is.</p>
<div class="panel-100 p-bottom-20">
<div class="form-group">
<label for="audio-devices"><i class="fa-solid fa-headphones"></i> STREAM AUDIO FROM:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-devices" type="text" name="audio-devices" placeholder="Choose your audio device" readonly />
<ul class="options" id="deviceList">
<% videoDevices.forEach(device => { %>
<li data-value="<%= device.name %>" class="option"><%= device.name %></li>
<% }); %>
<% audioDevices.forEach(device => { %>
<li data-value="<%= device.name %>" class="option"><%= device.name %></li>
<% }); %>
</ul>
</div>
</div>
<div class="form-group">
<label for="audio-devices"><i class="fa-solid fa-microphone-lines"></i> AUDIO CHANNELS:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-channels" type="text" name="audio-channels" placeholder="Stereo" readonly />
<ul class="options">
<li data-value="2" class="option">Stereo</li>
<li data-value="1" class="option">Mono</li>
</ul>
</div>
</div>
<div class="form-group">
<label for="audio-quality"><i class="fa-solid fa-wave-square"></i> AUDIO QUALITY:</label>
<div class="dropdown" style="width: 300px;">
<input id="audio-quality" type="text" name="audio-quality" placeholder="128k (standard)" readonly />
<ul class="options">
<li data-value="64k" class="option">64k (lowest quality)</li>
<li data-value="96k" class="option">96k (low quality)</li>
<li data-value="128k" class="option">128k (standard)</li>
<li data-value="192k" class="option">192k (higher quality)</li>
<li data-value="256k" class="option">256k (highest quality)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- AUDIO SETTINGS END -->
<!-- IDENTIFICATION START -->
<div id="step3" class="step" style="display: none;">
<div class="panel-100" style="padding-bottom: 20px;">
<h2 class="settings-heading">IDENTIFICATION INFO</h2>
<p class="text-gray">In this part, we will set up your indentification info, such as the server name, description and location.</p>
<label for="webserver-name" style="width: 100%;max-width: 768px; margin:auto;">Webserver name:</label>
<input style="width: 100%; max-width: 768px;" class="input-text" type="text" name="webserver-name" id="webserver-name" placeholder="Fill your server name here." maxlength="32">
<br>
<label for="webserver-desc" style="width: 100%;max-width: 768px; margin: auto;">Webserver description:</label>
<textarea id="webserver-desc" name="webserver-desc" placeholder="Fill the server description here. You can put useful info here such as your antenna setup. You can use simple markdown." maxlength="255"></textarea>
<h3>Location:</h3>
<p class="text-gray">Location info is useful for automatic identification of stations using RDS.</p>
<div class="form-group">
<label for="lat">Latitude:</label>
<input class="input-text" type="text" name="lat" id="lat">
</div>
<div class="form-group">
<label for="lng">Longitude:</label>
<input class="input-text" type="text" name="lng" id="lng">
</div>
<div id="map"></div>
<h3>Map broadcast:</h3>
<p class="m-0">If your location info is filled, you can add your tuner to a public list.</p>
<p class="m-0">The list is available at <strong><a href="https://list.fmdx.pl" target="_blank" class="color-4">list.fmdx.pl</a></strong>.</p>
<p class="text-gray">Only fill up your broadcast address if you are using a proxy. If you don't know what a proxy is, leave it empty.</p>
<div class="form-group checkbox">
<input type="checkbox" id="broadcast-tuner">
<label for="broadcast-tuner">Show my tuner on the public list</label>
</div>
<div class="form-group checkbox">
<input type="checkbox" id="tuner-public">
<label for="tuner-public">Allow tuning without password</label>
</div>
<br>
<div class="form-group">
<label for="broadcast-address">Broadcast address:</label>
<input class="input-text" type="text" name="broadcast-address" id="broadcast-address">
</div>
</div>
</div>
<!-- IDENTIFICATION END -->
<!-- ADMIN SETTINGS START -->
<div id="step4" class="step" style="display: none;">
<h2 class="settings-heading">Admin panel settings</h2>
<p>We are at the last and final step of the settings.</p>
<p class="text-gray">Here we can set the password. Tune password is optional.<br>Setting an admin password allows you to change settings later and setting one up is mandatory.</p>
<div class="form-group">
<label for="tune-pass">Tune password:</label>
<input class="input-text w-200" type="password" name="tune-pass" id="tune-pass">
</div>
<div class="form-group">
<label for="admin-pass">Admin setup password:</label>
<input class="input-text w-200" type="password" name="admin-pass" id="admin-pass">
</div><br>
<p>You can now click the <strong>save button</strong> to save your settings.</p>
</div>
<button class="btn-prev"><i class="fa-solid fa-arrow-left"></i></button>
<button class="btn-next">Next</button>
</div>
<div class="panel-100 no-bg">
<p>Feel free to contact us on <a href="https://discord.gg/ZAVNdS74mC" target="_blank"><strong><i class="fa-brands fa-discord"></i> Discord</strong></a> for community support.</p>
</div>
<% } else { %>
<div class="panel-100 no-bg">
<img class="top-25" src="../images/openradio_logo_neutral.png" height="64px">
<h2 class="text-monospace text-light">[ADMIN PANEL]</h2>
<p>You are currently not logged in as an administrator and therefore can't change the settings.</p>
<p>Please login below.</p>
</div>
<div class="panel-100 p-bottom-20">
<h2>LOGIN</h2>
<form action="./login" method="post" id="login-form">
<input style="background-color: var(--color-2);" type="password" id="password" name="password" required>
<button type="submit" class="br-0 w-100" style="height: 44px;">Login</button>
</form>
<div id="login-message"></div>
</div>
<% } %>
</div>
<script src="js/settings.js"></script>
<script src="js/dropdown.js"></script>
<script src="js/setup.js"></script>
<script src="js/wizard.js"></script>
<script src="js/confighandler.js"></script>
</body>
</html>