diff --git a/package.json b/package.json index 6bc1bdb..417e13a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.3.6.1", + "version": "1.3.7", "description": "FM DX Webserver", "main": "index.js", "scripts": { @@ -12,10 +12,10 @@ "author": "", "license": "ISC", "dependencies": { - "@mapbox/node-pre-gyp": "1.0.11", - "body-parser": "1.20.3", + "@mapbox/node-pre-gyp": "2.0.0", + "body-parser": "2.2.0", "ejs": "3.1.10", - "express": "4.21.2", + "express": "5.1.0", "express-session": "1.18.1", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", @@ -23,6 +23,6 @@ "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", - "ws": "8.18.0" + "ws": "8.18.1" } } diff --git a/server/endpoints.js b/server/endpoints.js index fa85f57..50ed624 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -179,21 +179,44 @@ router.get('/api', (req, res) => { }); +const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } } +const MAX_ATTEMPTS = 25; +const WINDOW_MS = 15 * 60 * 1000; + const authenticate = (req, res, next) => { + const ip = req.ip || req.connection.remoteAddress; + const now = Date.now(); + + if (!loginAttempts[ip]) { + loginAttempts[ip] = { count: 0, lastAttempt: now }; + } else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) { + loginAttempts[ip] = { count: 0, lastAttempt: now }; + } + + if (loginAttempts[ip].count >= MAX_ATTEMPTS) { + return res.status(403).json({ + message: 'Too many login attempts. Please try again later.' + }); + } + const { password } = req.body; - - // Check if the entered password matches the admin password + + loginAttempts[ip].lastAttempt = now; + if (password === serverConfig.password.adminPass) { req.session.isAdminAuthenticated = true; req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.'); + logInfo(`User from ${ip} logged in as an administrator.`); + loginAttempts[ip].count = 0; next(); } else if (password === serverConfig.password.tunePass) { req.session.isAdminAuthenticated = false; req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.'); + logInfo(`User from ${ip} logged in with tune permissions.`); + loginAttempts[ip].count = 0; next(); } else { + loginAttempts[ip].count += 1; res.status(403).json({ message: 'Login failed. Wrong password?' }); } }; diff --git a/server/helpers.js b/server/helpers.js index a6e2bef..4f7c98c 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -132,6 +132,7 @@ function fetchBannedAS(callback) { function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }; const connectionTime = new Date().toLocaleString([], options); + const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); fetchBannedAS((error, bannedAS) => { if (error) { @@ -155,7 +156,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { }); consoleCmd.logInfo( - `Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` + `Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` ); callback("User allowed"); diff --git a/server/index.js b/server/index.js index 6fd0f3a..4ee3ef9 100644 --- a/server/index.js +++ b/server/index.js @@ -388,6 +388,7 @@ wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; const userCommandHistory = {}; + const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); if (serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); @@ -509,7 +510,7 @@ wss.on('connection', (ws, request) => { } if (code !== 1008) { - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); + logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); } }); diff --git a/server/tx_search.js b/server/tx_search.js index cf48bcd..de31960 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -87,14 +87,11 @@ async function fetchTx(freq, piCode, rdsPs) { const now = Date.now(); freq = parseFloat(freq); - if (isNaN(freq)) { - return; - } + if (isNaN(freq)) return; if (now - lastFetchTime < fetchInterval || serverConfig.identification.lat.length < 2 || freq < 87 - || (currentPiCode == piCode && currentRdsPs == rdsPs)) - { + || (currentPiCode == piCode && currentRdsPs == rdsPs)) { return Promise.resolve(); } @@ -107,15 +104,33 @@ async function fetchTx(freq, piCode, rdsPs) { const url = "https://maps.fmdx.org/api/?freq=" + freq; try { - const response = await fetch(url, { redirect: 'manual' }); - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - const data = await response.json(); + // Try POST first + const postResponse = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ freq }), // You can omit or customize this + redirect: 'manual' + }); + + if (!postResponse.ok) throw new Error(`POST failed: ${postResponse.status}`); + const data = await postResponse.json(); cachedData[freq] = data; - if(serverConfig.webserver.rdsMode == true) await loadUsStatesGeoJson(); + if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); return processData(data, piCode, rdsPs); - } catch (error) { - console.error("Error fetching data:", error); - return null; // Return null to indicate failure + } catch (postError) { + console.warn("POST failed, trying GET:", postError); + + try { + const getResponse = await fetch(url, { redirect: 'manual' }); + if (!getResponse.ok) throw new Error(`GET failed: ${getResponse.status}`); + const data = await getResponse.json(); + cachedData[freq] = data; + if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); + return processData(data, piCode, rdsPs); + } catch (getError) { + console.error("GET also failed:", getError); + return null; + } } } diff --git a/web/_components.ejs b/web/_components.ejs index 64f003e..bc60799 100644 --- a/web/_components.ejs +++ b/web/_components.ejs @@ -29,10 +29,13 @@ switch (component) { * @param cssClass Custom CSS class if needed */ case 'checkbox': -%> -
- - +%> +
+
+ + + <%= label %> +
<% break; diff --git a/web/css/buttons.css b/web/css/buttons.css index 273eade..ab51976 100644 --- a/web/css/buttons.css +++ b/web/css/buttons.css @@ -308,6 +308,47 @@ input[type="range"]::-moz-range-thumb { /* End Toggle Switch */ +.switch { + user-select: none; +} +.switch input[type=checkbox] { + height: 0; + width: 0; + visibility: hidden; +} +.switch input[type=checkbox]:checked + label { + background: var(--color-4); +} +.switch input[type=checkbox]:checked + label::after { + left: calc(100% - 4px); + transform: translateX(-100%); + background-color: var(--color-1); +} +.switch label { + cursor: pointer; + min-width: 64px; + max-width: 64px; + height: 38px; + background-color: var(--color-1); + transition: 0.35s background-color; + display: block; + border-radius: 24px; + margin: 0; + position: relative; + border: 2px solid var(--color-3); +} +.switch label::after { + content: ""; + position: absolute; + top: 4px; + left: 4px; + width: 26px; + height: 26px; + background: var(--color-5); + border-radius: 16px; + transition: 0.3s; +} + select { height: 42px; width: 150px; diff --git a/web/css/helpers.css b/web/css/helpers.css index 9eae422..5aad2b3 100644 --- a/web/css/helpers.css +++ b/web/css/helpers.css @@ -119,6 +119,10 @@ align-items: center; } +.flex-column { + flex-direction: column; +} + .hover-brighten { transition: 0.3s ease background-color; } @@ -302,6 +306,10 @@ table .input-text { .flex-phone-column { flex-flow: column; } + .flex-phone-center { + align-items: center; + justify-content: center; + } .hide-phone { display: none; } diff --git a/web/css/modal.css b/web/css/modal.css index 0f4b9ed..8e9cd36 100644 --- a/web/css/modal.css +++ b/web/css/modal.css @@ -86,10 +86,6 @@ body.modal-open { background-color: var(--color-main); } -.modal-panel .flex-container { - align-items: stretch; -} - .modal-panel h1 { font-size: 42px; } @@ -124,7 +120,6 @@ body.modal-open { .modal-panel label { width: 200px; - margin: auto; } .modal-panel-chat { @@ -147,6 +142,37 @@ body.modal-open { border-radius: 15px 15px 0px 0px; } +/* Popup Windows */ +.popup-window { + width: 500px; + background: var(--color-1-transparent); + backdrop-filter: blur(5px); + position: absolute !important; + border-radius: 15px; + display: none; + overflow: hidden; + border: 3px solid var(--color-1); + z-index: 100; + } + + .popup-header { + padding: 8px; + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + } + + .popup-close { + background: none; + border: none; + color: var(--color-3); + font-size: 16px; + cursor: pointer; + max-width: 24px; + margin-left: auto; + } + @media only screen and (max-width: 768px) { .modal-content { min-width: 90% !important; @@ -165,11 +191,9 @@ body.modal-open { .modal-panel { width: 100%; } - .modal-panel-chat { - height: 510px; - } - #chat-chatbox { - height: 333px !important; + + .popup-window { + width: 90%; } } diff --git a/web/css/panels.css b/web/css/panels.css index 153af52..59dafcb 100644 --- a/web/css/panels.css +++ b/web/css/panels.css @@ -69,6 +69,9 @@ .panel-75 { width: 90%; } + .no-bg-phone { + background-color: transparent; + } .panel-33 h2 { padding: 20px; padding-top: 5px; @@ -81,7 +84,6 @@ margin: auto; width: 90%; margin-bottom: 20px; - background-color: transparent; } *[class^="panel-"]:not(.no-bg):not(.no-filter):not(#ps-container), .panel-100.w-100::before { diff --git a/web/index.ejs b/web/index.ejs index 45b3112..0800cc7 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -5,7 +5,10 @@ + + + @@ -178,7 +181,7 @@
-
+

PI CODE

  @@ -186,12 +189,12 @@
-
+

FREQUENCY

-
+

SIGNAL

@@ -334,7 +337,7 @@
-
+

RADIOTEXT

@@ -345,7 +348,7 @@
-

@@ -364,7 +367,7 @@

-
+

AF

@@ -373,7 +376,7 @@
-
+

@@ -394,6 +397,30 @@
+ +
- -
diff --git a/web/js/3las/main.js b/web/js/3las/main.js index f1dcaad..41f56d6 100644 --- a/web/js/3las/main.js +++ b/web/js/3las/main.js @@ -34,20 +34,17 @@ function OnConnectivityCallback(isConnected) { } } -function isIOS() { - return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; -} function OnPlayButtonClick(_ev) { const $playbutton = $('.playbutton'); - const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; if (Stream) { console.log("Stopping stream..."); shouldReconnect = false; destroyStream(); $playbutton.find('.fa-solid').toggleClass('fa-stop fa-play'); - if (isiOS && 'audioSession' in navigator) { + if (isAppleiOS && 'audioSession' in navigator) { navigator.audioSession.type = "none"; } } else { @@ -56,7 +53,7 @@ function OnPlayButtonClick(_ev) { createStream(); Stream.Start(); $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); - if (isiOS && 'audioSession' in navigator) { + if (isAppleiOS && 'audioSession' in navigator) { navigator.audioSession.type = "playback"; } } diff --git a/web/js/chat.js b/web/js/chat.js index 746eb7e..3917c74 100644 --- a/web/js/chat.js +++ b/web/js/chat.js @@ -10,9 +10,29 @@ $(document).ready(function() { const chatNicknameInput = $('#chat-nickname'); const chatNicknameSave = $('#chat-nickname-save'); + $(function () { + $("#popup-panel-chat").draggable({ + handle: ".popup-header" + }).resizable({ + minHeight: 300, + minWidth: 250 + }); + + $(".chatbutton").on("click", function () { + $("#popup-panel-chat").fadeIn(200, function () { + chatMessages.scrollTop(chatMessages[0].scrollHeight); + }); + }); + + $("#popup-panel-chat .popup-close").on("click", function () { + $("#popup-panel-chat").fadeOut(200); + }); + }); + + // Function to generate a random string function generateRandomString(length) { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const characters = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); @@ -21,7 +41,7 @@ $(document).ready(function() { } // Load nickname from localStorage on page load - let savedNickname = localStorage.getItem('nickname') || `Anonymous User ${generateRandomString(5)}`; + let savedNickname = localStorage.getItem('nickname') || `User ${generateRandomString(5)}`; chatNicknameInput.val(savedNickname); chatIdentityNickname.text(savedNickname); diff --git a/web/js/confighandler.js b/web/js/confighandler.js index e7bac21..533b524 100644 --- a/web/js/confighandler.js +++ b/web/js/confighandler.js @@ -96,8 +96,6 @@ function populateFields(data, prefix = "") { $element.val(value); } }); - - updateIconState(); } function updateConfigData(data, prefix = "") { diff --git a/web/js/init.js b/web/js/init.js index e672142..45adb5b 100644 --- a/web/js/init.js +++ b/web/js/init.js @@ -1,9 +1,9 @@ -var currentDate = new Date('Apr 19, 2025 21:30:00'); +var currentDate = new Date('Apr 22, 2025 21:30:00'); var day = currentDate.getDate(); var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1 var year = currentDate.getFullYear(); var formattedDate = day + '/' + month + '/' + year; -var currentVersion = 'v1.3.6.1 [' + formattedDate + ']'; +var currentVersion = 'v1.3.7 [' + formattedDate + ']'; getInitialSettings(); removeUrlParameters(); diff --git a/web/js/modal.js b/web/js/modal.js index 20bf901..299db06 100644 --- a/web/js/modal.js +++ b/web/js/modal.js @@ -2,7 +2,6 @@ $(document).ready(function() { // Cache jQuery objects for reuse var modal = $("#myModal"); var modalPanel = $(".modal-panel"); - var chatPanel = $(".modal-panel-chat"); var openBtn = $("#settings"); var closeBtn = $(".closeModal, .closeModalButton"); @@ -20,7 +19,6 @@ $(document).ready(function() { modal.css("opacity", 0); setTimeout(function() { modal.css("display", "none"); - modalPanel.add(chatPanel).css("display", "none"); $("body").removeClass("modal-open"); // Enable body scrolling }, 300); } @@ -30,10 +28,6 @@ $(document).ready(function() { openModal(modalPanel); }); - $(".chatbutton").on("click", function() { - openModal(chatPanel); - }); - closeBtn.on("click", closeModal); // Close the modal when clicking outside of it diff --git a/web/js/settings.js b/web/js/settings.js index 15d9ba1..829820b 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -166,11 +166,6 @@ $(document).ready(() => { $('.version-string').text(currentVersion); setBg(); - - // Update icons when the checkbox state changes - $('input[type="checkbox"]').change(function() { - updateIconState(this); - }); }); function getQueryParameter(name) { @@ -178,29 +173,6 @@ function getQueryParameter(name) { return urlParams.get(name); } -function updateIconState(el) { - // If an element is passed, update only that one - if (el) { - var $checkbox = $(el); - var icon = $checkbox.siblings('label').find('i'); - if ($checkbox.is(':checked')) { - icon.removeClass('fa-toggle-off').addClass('fa-toggle-on'); - } else { - icon.removeClass('fa-toggle-on').addClass('fa-toggle-off'); - } - } else { - // Otherwise, update all checkboxes - $('input[type="checkbox"]').each(function() { - var icon = $(this).siblings('label').find('i'); - if ($(this).is(':checked')) { - icon.removeClass('fa-toggle-off').addClass('fa-toggle-on'); - } else { - icon.removeClass('fa-toggle-on').addClass('fa-toggle-off'); - } - }); - } -} - function setTheme(themeName) { const themeColors = themes[themeName]; if (themeColors) { diff --git a/web/js/setup.js b/web/js/setup.js index b0cfba0..806b607 100644 --- a/web/js/setup.js +++ b/web/js/setup.js @@ -20,34 +20,48 @@ $(document).ready(function() { function mapCreate() { if (!(typeof map == "object")) { map = L.map('map', { - center: [40,0], + center: [40, 0], zoom: 3 }); + } else { + map.setZoom(3).panTo([40, 0]); } - else { - map.setZoom(3).panTo([40,0]); - } - + L.tileLayer(tilesURL, { attribution: mapAttrib, maxZoom: 19 }).addTo(map); + // Check for initial lat/lon values + const latVal = parseFloat($('#identification-lat').val()); + const lonVal = parseFloat($('#identification-lon').val()); + + if (!isNaN(latVal) && !isNaN(lonVal)) { + const initialLatLng = L.latLng(latVal, lonVal); + pin = L.marker(initialLatLng, { riseOnHover: true, draggable: true }).addTo(map); + map.setView(initialLatLng, 8); // Optional: Zoom in closer to the pin + + pin.on('dragend', function(ev) { + $('#identification-lat').val(ev.target.getLatLng().lat.toFixed(6)); + $('#identification-lon').val(ev.target.getLatLng().lng.toFixed(6)); + }); + } + map.on('click', function(ev) { - $('#identification-lat').val((ev.latlng.lat).toFixed(6)); - $('#identification-lon').val((ev.latlng.lng).toFixed(6)); - + $('#identification-lat').val(ev.latlng.lat.toFixed(6)); + $('#identification-lon').val(ev.latlng.lng.toFixed(6)); + if (typeof pin == "object") { pin.setLatLng(ev.latlng); } else { - pin = L.marker(ev.latlng,{ riseOnHover:true,draggable:true }); - pin.addTo(map); - pin.on('dragend',function(ev) { - $('#identification-lat').val((ev.latlng.lat).toFixed(6)); - $('#identification.lon').val((ev.latlng.lng).toFixed(6)); + pin = L.marker(ev.latlng, { riseOnHover: true, draggable: true }).addTo(map); + pin.on('dragend', function(ev) { + $('#identification-lat').val(ev.target.getLatLng().lat.toFixed(6)); + $('#identification-lon').val(ev.target.getLatLng().lng.toFixed(6)); }); } }); + mapReload(); } diff --git a/web/setup.ejs b/web/setup.ejs index cc83e44..0d88214 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -85,6 +85,7 @@
+

Current users

@@ -118,7 +119,9 @@
+
+

Quick settings

@@ -129,6 +132,7 @@ <%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Tune password', id: 'password-tunePass', password: true}) %> <%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Admin password', id: 'password-adminPass', password: true}) %>
+
@@ -223,9 +227,10 @@

Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.

<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
-
-

Samplerate Offset

-

Using a negative value could eliminate audio buffering issues during long periods of listening. However, a value that’s too low might increase the buffer over time.

+
+

Sample rate Offset

+

Using a negative value could eliminate audio buffering issues during long periods of listening.
+ However, a value that’s too low might increase the buffer over time.

@@ -234,13 +239,13 @@

Webserver settings

-
+

Connection

Leave the IP at 0.0.0.0 unless you explicitly know you have to change it.
Don't enter your public IP here.

<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '0.0.0.0', label: 'Webserver IP', id: 'webserver-webserverIp'}) %> <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: '8080', label: 'Webserver port', id: 'webserver-webserverPort'}) %>
-
+

Design

Background image

<%- include('_components', {component: 'text', cssClass: 'br-15', placeholder: 'Direct image link', label: 'Image link', id: 'webserver-bgImage'}) %>
@@ -260,21 +265,33 @@ ] }) %>
-
+
+
+

Antennas

<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', label: 'Antenna switch', id: 'antennas-enabled'}) %>
- <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 1', id: 'antennas-ant1-enabled'}) %> - <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant A', label: 'Antenna 1 name', id: 'antennas-ant1-name'}) %>
- - <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 2', id: 'antennas-ant2-enabled'}) %> - <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant B', label: 'Antenna 2 name', id: 'antennas-ant2-name'}) %>
- - <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 3', id: 'antennas-ant3-enabled'}) %> - <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant C', label: 'Antenna 3 name', id: 'antennas-ant3-name'}) %>
- - <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 4', id: 'antennas-ant4-enabled'}) %> - <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant D', label: 'Antenna 4 name', id: 'antennas-ant4-name'}) %>
+
+
+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 1', id: 'antennas-ant1-enabled'}) %> + <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant A', label: 'Antenna 1 name', id: 'antennas-ant1-name'}) %>
+
+ +
+ <%- include('_components', {component: 'checkbox', cssClass: 'top-25', label: 'Antenna 2', id: 'antennas-ant2-enabled'}) %> + <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant B', label: 'Antenna 2 name', id: 'antennas-ant2-name'}) %>
+
+ +
+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 3', id: 'antennas-ant3-enabled'}) %> + <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant C', label: 'Antenna 3 name', id: 'antennas-ant3-name'}) %>
+
+ +
+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Antenna 4', id: 'antennas-ant4-enabled'}) %> + <%- include('_components', {component: 'text', cssClass: 'w-100 br-15', placeholder: 'Ant D', label: 'Antenna 4 name', id: 'antennas-ant4-name'}) %>
+
+
@@ -303,12 +320,12 @@
-
+

RDS Mode

You can switch between American (RBDS) / Global (RDS) mode here.

<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>
-
+

Transmitter Search Algorithm

Different modes may help with more accurate transmitter identification depending on your region.

<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1', @@ -553,9 +570,9 @@ When you become a supporter, you can message the Founders on Discord for your login details.

<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %>
- <%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Subdomain name', id: 'tunnel-subdomain'}) %>.fmtuner.org
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %> <%- include('_components', {component: 'text', cssClass: 'w-250 br-15', password: true, placeholder: '', label: 'Token', id: 'tunnel-token'}) %> + <%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Subdomain name', id: 'tunnel-subdomain'}) %>.fmtuner.org

Enabling low latency mode may provide better experience, however it will also use more bandwidth.

<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Low latency mode', id: 'tunnel-lowLatencyMode'}) %>