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

Add files via upload

This commit is contained in:
Marek Farkaš
2024-01-14 14:36:39 +01:00
committed by GitHub
parent 6f2d31626c
commit 5b37848e35
6 changed files with 817 additions and 0 deletions

104
web/index.html Normal file
View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<title>FM-DX Webserver [Noobish's Server]</title>
<link href="/styles.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">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="wrapper">
<canvas id="signal-canvas" width="1024" height="200" style="margin-top: 120px;"></canvas>
<div class="flex-container">
<div class="panel-75" style="height: 110px;">
<span class="text-big" id="data-ps"></span>
</div>
<div class="panel-33">
<h2 style="padding-top: 18px;">
<span id="data-pty" style="color: #eee;"></span>
<span style="margin-left: 30px;" id="data-tp">TP</span>
<span style="margin-left: 15px; color: #ff5776;" id="data-st"></span>
</h2>
</div>
</div>
<div class="flex-container">
<div class="panel-33">
<h2>PI CODE</h2>
<span id="data-pi" class="text-big"></span>
</div>
<div class="panel-33">
<h2>FREQUENCY</h2>
<span id="data-frequency" class="text-big"></span>
</div>
<div class="panel-33">
<h2>SIGNAL</h2>
<span class="text-big">
<span id="data-signal"></span>
<span id="signal-units" class="text-medium-big">dBf</span>
</span>
</div>
</div>
<div class="flex-container">
<div class="panel-33" style="height: 48px;">
<audio id="myAudio" preload="none" autoplay></audio>
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1">
</div>
<div class="panel-33" style="opacity: 0;">
<button id="playButton">play</button>
</div>
<div class="panel-33" style="height: 48px;">
<label class="toggleSwitch nolabel" onclick="">
<input id="signal-units-toggle" type="checkbox"/>
<a></a>
<span>
<span class="left-span">dBf</span>
<span class="right-span">dBµV</span>
</span>
</label>
</div>
</div>
<div class="panel-100">
<h2 style="margin: 0;">RADIOTEXT</h2>
<div id="data-rt0"></div>
<div id="data-rt1"></div>
<div id="data-container" style="display: none;"></div>
</div>
<div id="data-af" style="text-align: center;"></div>
</div>
<button id="settings"><i class="fa-solid fa-gear"></i></button>
<div id="myModal" class="modal">
<div class="modal-content">
<span class="modal-title">Settings</span>
<span class="close" id="closeModal"><i class="fa-solid fa-xmark"></i></span>
<label for="themes" style="margin-top: 50px;"><i class="fa-solid fa-palette"></i> Theme:</label>
<select name="themes" style="margin-bottom: 50px;" id="theme-selector">
<option value="theme1">Blurple</option>
<option value="theme2">Red</option>
<option value="theme3">Green</option>
<option value="theme4">Cyan</option>
<option value="theme5">Orange</option>
</select>
<p class="text-small" style="margin-bottom: 50px;">FM-DX WebServer uses librds by <a href="https://fmdx.pl" target="_blank">Konrad Kosmatka</a>.</p>
<button class="button-close" id="closeModalButton">Close</button>
</div>
</div>
<script src="main.js"></script>
<script src="themes.js"></script>
<script src="modal.js"></script>
<script src="stream.js"></script>
</body>
</html>

168
web/main.js Normal file
View File

@@ -0,0 +1,168 @@
const hostParts = window.location.host.split(':');
const hostname = hostParts[0]; // Extract the hostname
const port = hostParts[1] || '8080'; // Extract the port or use a default (e.g., 8080)
const socketAddress = `ws://${hostname}:${port}/text`; // Use 'wss' for secure WebSocket connections (recommended for external access)
const socket = new WebSocket(socketAddress);
const dataContainer = document.querySelector('#data-container');
const canvas = document.querySelector('#signal-canvas');
const context = canvas.getContext('2d');
var signalToggle = document.getElementById("signal-units-toggle");
canvas.width = canvas.parentElement.clientWidth;
const data = [];
const maxDataPoints = 250;
const pointWidth = (canvas.width - 80) / maxDataPoints;
var europe_programmes = [
"No PTY", "News", "Current Affairs", "Info",
"Sport", "Education", "Drama", "Culture", "Science", "Varied",
"Pop M", "Rock M", "Easy Listening", "Light Classical",
"Serious Classical", "Other Music", "Weather", "Finance",
"Children's Programmes", "Social Affairs", "Religion", "Phone-in",
"Travel", "Leisure", "Jazz Music", "Country Music", "National Music",
"Oldies Music", "Folk Music", "Documentary", "Alarm Test"
];
// Function to handle zoom in
function zoomIn() {
zoomMinValue *= 0.9;
zoomMaxValue *= 0.9;
}
// Function to handle zoom out
function zoomOut() {
zoomMinValue *= 1.1;
zoomMaxValue *= 1.1;
}
function updateCanvas() {
// Remove old data when it exceeds the maximum data points
const color2 = getComputedStyle(document.documentElement).getPropertyValue('--color-2').trim();
const color4 = getComputedStyle(document.documentElement).getPropertyValue('--color-4').trim();
while (data.length >= maxDataPoints) {
data.shift();
}
// Modify the WebSocket onmessage callback
socket.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
updatePanels(parsedData);
// Push the new signal data to the array
data.push(parsedData.signal);
const actualLowestValue = Math.min(...data);
const actualHighestValue = Math.max(...data);
zoomMinValue = actualLowestValue - ((actualHighestValue - actualLowestValue) / 2);
zoomMaxValue = actualHighestValue + ((actualHighestValue - actualLowestValue) / 2);
zoomAvgValue = (zoomMaxValue - zoomMinValue) / 2 + zoomMinValue;
// Clear the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Draw the signal graph with zoom
context.beginPath();
context.moveTo(50, canvas.height - (data[0] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue)));
for (let i = 1; i < data.length; i++) {
const x = i * pointWidth;
const y = canvas.height - (data[i] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
context.lineTo(x + 40, y);
}
context.strokeStyle = color4;
context.lineWidth = 1;
context.stroke();
// Draw horizontal lines for lowest, highest, and average values
context.strokeStyle = color2; // Set line color
context.lineWidth = 1;
// Draw the lowest value line
const lowestY = canvas.height - (zoomMinValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
context.beginPath();
context.moveTo(40, lowestY - 18);
context.lineTo(canvas.width - 40, lowestY - 18);
context.stroke();
// Draw the highest value line
const highestY = canvas.height - (zoomMaxValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
context.beginPath();
context.moveTo(40, highestY + 10);
context.lineTo(canvas.width - 40, highestY + 10);
context.stroke();
const avgY = canvas.height / 2;
context.beginPath();
context.moveTo(40, avgY - 7);
context.lineTo(canvas.width - 40, avgY - 7);
context.stroke();
// Label the lines with their values
context.fillStyle = color4;
context.font = '12px Titillium Web';
const offset = signalToggle.checked ? 11.75 : 0;
context.textAlign = 'right';
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, 35, lowestY - 14);
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, 35, highestY + 14);
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, 35, avgY - 3);
context.textAlign = 'left';
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, canvas.width - 35, lowestY - 14);
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, canvas.width - 35, highestY + 14);
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, canvas.width - 35, avgY - 3);
// Update the data container with the latest data
dataContainer.innerHTML = event.data + '<br>';
};
requestAnimationFrame(updateCanvas);
}
// Start updating the canvas
updateCanvas();
function compareNumbers(a, b) {
return a - b;
}
function divideByHundred(a) {
a = a / 100;
}
function updatePanels(parsedData) {
sortedAf = parsedData.af.sort(compareNumbers);
sortedAf.forEach((element, index, array) => {
array[index] = element / 1000;
// Check if it's the last element in the array
if (index === array.length - 1) {
document.querySelector('#data-af').innerHTML = array;
}
});
document.querySelector('#data-frequency').textContent = parsedData.freq;
document.querySelector('#data-pi').innerHTML = parsedData.pi === '?' ? "<span class='text-gray'>?</span>" : parsedData.pi;
document.querySelector('#data-ps').innerHTML = parsedData.ps === '?' ? "<span class='text-gray'>?</span>" : parsedData.ps;
document.querySelector('#data-tp').innerHTML = parsedData.tp === false ? "<span class='text-gray'>TP</span>" : "TP";
document.querySelector('#data-pty').innerHTML = europe_programmes[parsedData.pty];
document.querySelector('#data-st').innerHTML = parsedData.st === false ? "<span class='text-gray'>ST</span>" : "ST";
document.querySelector('#data-rt0').innerHTML = parsedData.rt0;
document.querySelector('#data-rt1').innerHTML = parsedData.rt1;
document.querySelector('#data-signal').textContent = signalToggle.checked ? (parsedData.signal - 11.75).toFixed(1) : parsedData.signal;
}
signalToggle.addEventListener("change", function() {
signalText = document.querySelector('#signal-units');
if (signalToggle.checked) {
signalText.textContent = 'dBµV';
} else {
// Checkbox is unchecked
signalText.textContent = 'dBf';
}
});

33
web/modal.js Normal file
View File

@@ -0,0 +1,33 @@
// Get the modal element and the buttons to open and close it
var modal = document.getElementById("myModal");
var openBtn = document.getElementById("settings");
var closeBtn = document.getElementById("closeModal");
var closeBtnFull = document.getElementById("closeModalButton");
// Function to open the modal
function openModal() {
modal.style.display = "block";
setTimeout(function() {
modal.style.opacity = 1;
}, 10);
}
// Function to close the modal
function closeModal() {
modal.style.opacity = 0;
setTimeout(function() {
modal.style.display = "none";
}, 300); // This delay should match the transition duration (0.3s).
}
// Event listeners for the open and close buttons
openBtn.addEventListener("click", openModal);
closeBtn.addEventListener("click", closeModal);
closeBtnFull.addEventListener("click", closeModal);
// Close the modal when clicking outside of it
window.addEventListener("click", function(event) {
if (event.target == modal) {
closeModal();
}
});

20
web/stream.js Normal file
View File

@@ -0,0 +1,20 @@
const audioElement = document.getElementById("myAudio");
const volumeSlider = document.getElementById("volumeSlider");
const audioStream = "/audio-proxy";
const uniqueTimestamp = Date.now(); // Create a unique timestamp
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(audioContext.destination);
// Set the audio element's source to your external audio stream
audioElement.src = `${audioStream}?${uniqueTimestamp}`;
audioElement.play();
volumeSlider.addEventListener("input", (event) => {
event.stopPropagation();
audioElement.volume = volumeSlider.value;
});

443
web/styles.css Normal file
View File

@@ -0,0 +1,443 @@
@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap');
:root {
--color-main: #1d1838;
--color-main-bright: #8069fa;
--color-1: color-mix(in srgb, var(--color-main) 95%, var(--color-main-bright));
--color-2: color-mix(in srgb, var(--color-main) 75%, var(--color-main-bright));
--color-3: color-mix(in srgb, var(--color-main) 50%, var(--color-main-bright));
--color-4: color-mix(in srgb, var(--color-main) 20%, var(--color-main-bright));
--color-5: color-mix(in srgb, var(--color-main) 0%, var(--color-main-bright));
}
::selection {
background: var(--color-main-bright);
color: inherit;
}
body {
font-family: 'Titillium Web', sans-serif;
color: white;
background: var(--color-main);
}
a {
text-decoration: none;
color: white;
}
#data-pi {
text-transform: uppercase;
}
#wrapper {
margin: auto;
width: auto;
max-width: 1024px;
}
#color-settings, #settings {
background: transparent;
border: 0;
color: white;
position: absolute;
top: 15px;
right: 15px;
font-size: 16px;
width: 64px;
height: 64px;
line-height: 64px;
text-align: center;
border-radius: 50%;
transition: 500ms ease-in-out background;
cursor: pointer;
}
#color-settings {
top: 96px;
}
#settings:hover, #color-settings:hover {
background: var(--color-3);
}
h2 {
color: var(--color-4);
margin-bottom: 0;
}
.flex-container {
display: flex;
}
.panel-33 {
width: 33%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
}
.panel-75 {
width: 68%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
}
.panel-100 {
width: 98%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
min-height: 100px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
}
.auto {
margin: auto;
}
.text-big {
font-size: 72px;
font-weight: 600;
}
.text-medium-big {
font-size: 24px;
color: #aaa;
font-weight: 300;
}
.text-small {
font-size: 13px;
}
.text-gray {
color: #666;
}
.bg-dark {
background: #100d1f;
}
@media only screen and (max-width: 768px) {
.flex-container {
display: block;
}
.modal-content {
max-width: 90%;
margin: auto;
}
.panel-33 {
width: 90%;
margin: auto;
margin-bottom: 20px;
}
.panel-75 {
margin: 80px auto 0 auto !important;
width: 90%;
}
.panel-33 h2 {
padding: 20px;
}
.text-big {
font-size: 54px;
display: block;
margin-top: -50px;
}
.panel-100 {
width: 90%;
margin: auto;
}
}
input[type="range"] {
margin: 0;
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
/* creating a custom design */
width: 100%;
cursor: pointer;
outline: none;
/* slider progress trick */
overflow: hidden;
border-radius: 30px;
height: 100%;
background: transparent;
}
/* Track: Mozilla Firefox */
input[type="range"]::-moz-range-track {
height: 48px;
background: var(--color-1);
border-radius: 30px;
border: 0;
}
/* Thumb: webkit */
input[type="range"]::-webkit-slider-thumb {
/* removing default appearance */
-webkit-appearance: none;
appearance: none;
/* creating a custom design */
height: 48px;
width: 48px;
background-color: #fff;
border-radius: 10px;
border: 2px solid var(--color-4);
/* slider progress trick */
box-shadow: -407px 0 0 400px var(--color-4);
}
/* Thumb: Firefox */
input[type="range"]::-moz-range-thumb {
box-sizing: border-box;
height: 48px;
width: 48px;
background-color: var(--color-4);
border-radius: 0px 30px 30px 0px;
border: 0;
outline: none;
/* slider progress trick */
box-shadow: -420px 0 0 400px var(--color-4);
}
/* Toggle Switch */
.toggleSwitch span span {
display: none;
}
.toggleSwitch {
user-select: none;
display: inline-block;
height: 48px;
position: relative;
overflow: hidden;
padding: 0;
cursor: pointer;
width: 100%;
border-radius: 25px;
font-weight: bold;
}
.toggleSwitch * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.toggleSwitch input:focus ~ a,
.toggleSwitch input:focus + label {
outline: none;
}
.toggleSwitch label {
position: relative;
z-index: 3;
display: block;
width: 100%;
}
.toggleSwitch input {
position: absolute;
opacity: 0;
z-index: 5;
}
.toggleSwitch > span {
position: absolute;
left: 0;
width: calc(100% - 6px);
margin: 0;
text-align: left;
white-space: nowrap;
margin:0;
}
.toggleSwitch > span span {
position: absolute;
top: 0;
left: 0;
z-index: 5;
display: block;
width: 50%;
margin-left: 50px;
text-align: left;
font-size: 0.9em;
width: auto;
opacity: 1;
width: 40%;
text-align: center;
line-height:48px;
}
.toggleSwitch a {
position: absolute;
right: 50%;
z-index: 4;
display: block;
top: 0;
bottom: 0;
padding: 0;
left: 0;
width: 50%;
background-color: var(--color-4);
border-radius: 25px;
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.toggleSwitch > span span:first-of-type {
color: var(--color-1);
opacity: 1;
left: 0;
margin: 0;
width: 50%;
}
.toggleSwitch > span span:last-of-type {
left:auto;
right:0;
color: var(--color-4);
margin: 0;
width: 50%;
}
.toggleSwitch > span:before {
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: -2px;
border-radius: 30px;
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
}
.toggleSwitch input:checked ~ a {
left: calc(50% - 3px);
}
.toggleSwitch input:checked ~ span span:first-of-type {
left:0;
color: var(--color-4);
}
.toggleSwitch input:checked ~ span span:last-of-type {
color: var(--color-1);
}
/* End Toggle Switch */
/* Style for the modal container */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent background */
opacity: 0;
transition: opacity 0.3s ease-in-out; /* Fade-in/out transition */
z-index: 20; /* Ensure the modal is above other content */
color: var(--color-4);
backdrop-filter: blur(10px);
}
/* Style for the modal content */
.modal-content {
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--color-main);
padding: 30px;
border-radius: 30px;
opacity: 1;
transition: opacity 0.3s ease-in-out; /* Fade-in/out transition */
z-index: 21; /* Ensure the modal content is above the modal background */
min-width: 500px;
}
.modal-content p {
margin: 0;
}
.modal-title {
font-size: 20px;
position: absolute;
font-weight: 300;
top: 14px;
left: 30px;
}
/* Style for the close button */
.close {
position: absolute;
top: 17px;
right: 30px;
cursor: pointer;
transition: 0.3s ease-in-out color;
}
.close:hover {
color: white;
}
.modal-content .button-close {
position: absolute;
bottom: 25px;
right: 35px;
width: 100px;
height: 48px;
border-radius: 30px;
background: var(--color-4);
font-weight: bold;
border: 0;
transition: 0.35s ease-in-out background;
cursor: pointer;
}
.modal-content .button-close:hover {
background: var(--color-5);
}
.modal label {
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
display: block;
}
.themes-list select {
display: none;
}
#theme-selector {
height: 42px;
width: 150px;
padding: 10px;
background: var(--color-4);
color: var(--color-1);
border: 0;
border-bottom: 4px solid var(--color-2);
cursor: pointer;
transition: 0.35s ease-in-out background;
}
#theme-selector:hover {
background: var(--color-5);
}

49
web/themes.js Normal file
View File

@@ -0,0 +1,49 @@
const themes = {
theme1: {
'--color-main': '#1d1838',
'--color-main-bright': '#8069fa',
},
theme2: {
'--color-main': '#381818',
'--color-main-bright': '#ff7070',
},
theme3: {
'--color-main': '#121c0c',
'--color-main-bright': '#a9ff70',
},
theme4: {
'--color-main': '#0c1c1b',
'--color-main-bright': '#68f7ee',
},
theme5: {
'--color-main': '#171106',
'--color-main-bright': '#f5b642',
}
};
function setTheme(themeName) {
const theme = themes[themeName];
if (theme) {
for (const [variable, value] of Object.entries(theme)) {
document.documentElement.style.setProperty(variable, value);
}
}
}
// Get the dropdown element
const themeSelector = document.getElementById('theme-selector');
const savedTheme = localStorage.getItem("theme");
if(savedTheme) {
setTheme(savedTheme);
themeSelector.value = savedTheme;
}
// Listen for changes in the dropdown
themeSelector.addEventListener('change', (event) => {
const selectedTheme = event.target.value;
setTheme(selectedTheme);
localStorage.setItem("theme", selectedTheme);
});