1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +01:00
This commit is contained in:
NoobishSVK
2024-01-22 20:33:45 +01:00
parent c72153763e
commit 5e3a0a466c
21 changed files with 1125 additions and 1180 deletions

17
console.js Normal file
View File

@@ -0,0 +1,17 @@
const { verboseMode } = require('./userconfig');
const MESSAGE_PREFIX = {
INFO: "\x1b[32m[INFO]\x1b[0m",
DEBUG: "\x1b[36m[DEBUG]\x1b[0m",
};
const logInfo = (...messages) => console.log(MESSAGE_PREFIX.INFO, ...messages);
const logDebug = (...messages) => {
if (verboseMode) {
console.log(MESSAGE_PREFIX.DEBUG, ...messages);
}
};
module.exports = {
logInfo, logDebug
}

View File

@@ -1,3 +1,4 @@
/* Libraries / Imports */
const koffi = require('koffi');
const path = require('path');
const os = require('os');
@@ -5,6 +6,8 @@ const win32 = (os.platform() == "win32");
const unicode_type = (win32 ? 'int16_t' : 'int32_t');
const lib = koffi.load(path.join(__dirname, "librdsparser." + (win32 ? "dll" : "so")));
var rdsBuffer = [];
koffi.proto('void callback_pi(void *rds, void *user_data)');
koffi.proto('void callback_pty(void *rds, void *user_data)');
koffi.proto('void callback_tp(void *rds, void *user_data)');
@@ -62,21 +65,6 @@ const rdsparser = {
country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)')
}
const decode_unicode = function(string)
{
let content = rdsparser.string_get_content(string);
let length = rdsparser.string_get_length(string);
let array = koffi.decode(content, koffi.array(unicode_type, length));
return Buffer.from(array, 'utf-8').toString();
};
const decode_errors = function(string) {
let errors = rdsparser.string_get_errors(string);
let length = rdsparser.string_get_length(string);
let array = koffi.decode(errors, koffi.array('uint8_t', length));
return Uint8Array.from(array).toString();
};
const callbacks = {
pi: koffi.register(rds => (
value = rdsparser.get_pi(rds)
@@ -178,6 +166,21 @@ rdsparser.register_rt(rds, callbacks.rt);
rdsparser.register_ptyn(rds, callbacks.ptyn);
rdsparser.register_ct(rds, callbacks.ct);
const decode_unicode = function(string)
{
let content = rdsparser.string_get_content(string);
let length = rdsparser.string_get_length(string);
let array = koffi.decode(content, koffi.array(unicode_type, length));
return Buffer.from(array, 'utf-8').toString();
};
const decode_errors = function(string) {
let errors = rdsparser.string_get_errors(string);
let length = rdsparser.string_get_length(string);
let array = koffi.decode(errors, koffi.array('uint8_t', length));
return Uint8Array.from(array).toString();
};
const updateInterval = 75;
const clientUpdateIntervals = new Map(); // Store update intervals for each client
@@ -197,27 +200,9 @@ var dataToSend = {
country_iso: 'UN',
users: '',
};
const initialData = {
pi: '?',
freq: 87.500.toFixed(3),
signal: 0,
st: false,
ps: '',
tp: false,
pty: 0,
af: [],
rt0: '',
rt1: '',
country_name: '',
country_iso: 'UN',
users: ''
};
const initialData = { ...dataToSend };
const resetToDefault = dataToSend => Object.assign(dataToSend, initialData);
var rdsBuffer = [];
function handleBuffer() {
for (let group of rdsBuffer)
{
@@ -233,15 +218,13 @@ function handleData(ws, receivedData) {
const receivedLines = receivedData.split('\n');
for (const receivedLine of receivedLines) {
switch (true) {
case receivedLine.startsWith('P'):
modifiedData = receivedLine.slice(1);
if (dataToSend.pi.length > modifiedData.length || dataToSend.pi == '?') {
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') {
dataToSend.pi = modifiedData;
}
break;
case receivedLine.startsWith('T'):
rdsBuffer = [];
resetToDefault(dataToSend);
@@ -281,8 +264,8 @@ function handleData(ws, receivedData) {
if (rdsBuffer.length > 1000) {
rdsBuffer.shift();
}
rdsBuffer.push(modifiedData);
//console.log("\"" + modifiedData + "\",");
if (rdsBuffer.length > 1) {
handleBuffer();

View File

@@ -1,41 +1,34 @@
// Libraries
/* Libraries / Imports */
const express = require('express');
const app = express();
const http = require('http');
const httpServer = http.createServer(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ noServer: true });
const path = require('path');
const net = require('net');
const client = new net.Socket();
const crypto = require('crypto');
const dataHandler = require('./datahandler');
const consoleCmd = require('./console');
const config = require('./userconfig');
/* Server settings */
const { webServerHost, webServerPort, webServerName, xdrdServerHost, xdrdServerPort, xdrdPassword, qthLatitude, qthLongitude } = config;
const infoMsg = "\x1b[32m[INFO]\x1b[0m";
const debugMsg = "\x1b[36m[DEBUG]\x1b[0m";
const { logInfo, logDebug } = consoleCmd;
let receivedSalt = '';
let receivedPassword = false;
let currentUsers = 0;
const wss = new WebSocket.Server({ noServer: true });
const app = express();
const httpServer = http.createServer(app);
/* connection to xdrd */
const client = new net.Socket();
/* webSocket handlers */
wss.on('connection', (ws, request) => {
const clientIp = request.connection.remoteAddress;
currentUsers++;
dataHandler.showOnlineUsers(currentUsers);
console.log(infoMsg, `Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
ws.on('message', (message) => {
if(config.verboseMode === true) {
console.log(debugMsg,'Received message from client:', message.toString());
}
consoleCmd.logDebug('Received message from client:', message.toString());
newFreq = message.toString() * 1000;
client.write("T" + newFreq + '\n');
});
@@ -43,14 +36,14 @@ wss.on('connection', (ws, request) => {
ws.on('close', (code, reason) => {
currentUsers--;
dataHandler.showOnlineUsers(currentUsers);
console.log(infoMsg, `Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
consoleCmd.logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
});
ws.on('error', console.error);
});
// Serve static files from the "web" folder
/* Serving of HTML files */
app.use(express.static(path.join(__dirname, 'web')));
// Function to authenticate with the xdrd server
@@ -68,7 +61,7 @@ function authenticateWithXdrd(client, salt, password) {
// WebSocket client connection
client.connect(xdrdServerPort, xdrdServerHost, () => {
console.log(infoMsg, 'Connected to xdrd successfully.');
consoleCmd.logInfo('Connected to xdrd successfully.');
client.once('data', (data) => {
const receivedData = data.toString();
@@ -107,10 +100,10 @@ httpServer.on('upgrade', (request, socket, head) => {
});
httpServer.listen(webServerPort, webServerHost, () => {
console.log(infoMsg, `Web server is running at \x1b[34mhttp://${webServerHost}:${webServerPort}\x1b[0m.`);
consoleCmd.logInfo(`Web server is running at \x1b[34mhttp://${webServerHost}:${webServerPort}\x1b[0m.`);
});
/* Static data are being sent through here on connection - these don't change when the server is running */
app.get('/static_data', (req, res) => {
res.json({ qthLatitude, qthLongitude, webServerName });
});

View File

@@ -9,7 +9,7 @@ const xdrdPassword = 'changememe'; // xdrd password (optional)
const qthLatitude = '50.357935'; // your latitude, useful for maps.fmdx.pl integration
const qthLongitude = '15.924395'; // your longitude, useful for maps.fmdx.pl integration
const verboseMode = false; // if true, console will display extra messages
const verboseMode = true; // if true, console will display extra messages
// DO NOT MODIFY ANYTHING BELOW THIS LINE
module.exports = {

47
web/css/breadcrumbs.css Normal file
View File

@@ -0,0 +1,47 @@
h2 {
color: var(--color-4);
margin-bottom: 0;
}
h3 {
font-size: 22px;
}
#data-ps, #data-rt0, #data-rt1 {
font-family: monospace;
}
#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);
}
#af-list ul {
display:list-item;
padding: 0;
list-style-type: none;
height: 425px;
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: 0;
}

241
web/css/buttons.css Normal file
View File

@@ -0,0 +1,241 @@
#tune-buttons input[type="text"] {
width: 50%;
height: 100%;
min-height: 46px;
padding-left: 20px;
box-sizing: border-box;
border: 2px solid transparent;
outline: 0;
color: white;
background-color: var(--color-1);
font-family: 'Titillium Web', sans-serif;
}
input[type="text"]:hover {
border: 2px solid var(--color-main-bright);
}
#tune-buttons button {
box-sizing: border-box;
background-color: var(--color-4);
border: 0;
color: var(--color-1);
width: 25%;
height: 100%;
display: block;
padding: 14px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
#tune-buttons button:hover {
background-color: var(--color-main-bright);
}
#freq-down {
border-radius: 30px 0 0 30px;
}
#freq-up {
border-radius: 0 30px 30px 0;
}
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 */
select {
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;
font-family: inherit;
font-weight: bold;
}
select option {
font-family: 'Titillium Web', sans-serif;
font-weight: 300;
padding: 10px;
border: 0;
}
select:hover {
background: var(--color-5);
}

7
web/css/entry.css Normal file
View File

@@ -0,0 +1,7 @@
@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');
@import url("main.css"); /* Root stuff that affects the entire webpage (body, wrapper etc.) */
@import url("breadcrumbs.css"); /* Stuff that applies to random elements only once/twice */
@import url("buttons.css"); /* Buttons, inputs, select boxes, checkboxes... */
@import url("helpers.css"); /* Stuff that is used often such as text changers etc */
@import url("panels.css"); /* Different panels and their sizes */
@import url("modal.css"); /* Modal window */

89
web/css/helpers.css Normal file
View File

@@ -0,0 +1,89 @@
.auto {
margin: auto;
}
.bg-dark {
background: #100d1f;
}
.bg-none {
background: transparent !important;
}
.color-1 {
color: var(--color-1);
}
.color-2 {
color: var(--color-2);
}
.color-3 {
color: var(--color-3);
}
.color-4 {
color: var(--color-4);
}
.flex-container {
display: flex;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.hover-brighten:hover {
cursor: pointer;
background-color: var(--color-2);
}
.text-big {
font-size: 60px;
font-weight: 300;
}
.text-medium {
font-size: 24px;
color: #aaa;
font-weight: 300;
}
.text-medium-big {
font-size: 46px;
}
.text-small {
font-size: 13px;
}
.text-gray {
color: #666;
}
.text-uppercase {
text-transform: uppercase;
}
@media only screen and (max-width: 768px) {
.flex-container {
display: block;
}
.flex-phone {
display: flex;
}
.text-medium-big {
font-size: 32px;
}
.text-big {
font-size: 40px;
display: block;
margin-top: -25px;
}
.text-big#data-ps {
margin: 0;
}
}

50
web/css/main.css Normal file
View File

@@ -0,0 +1,50 @@
:root {
--color-main: #111;
--color-main-bright: #aaa;
--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));
}
* {
box-sizing: border-box;
}
::selection {
background: var(--color-main-bright);
color: inherit;
}
body {
font-family: 'Titillium Web', sans-serif;
color: white;
background-color: var(--color-main);
transition: 0.3s ease-in-out background-color;
}
#wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: auto;
max-width: 1240px;
}
@media (max-width: 960px) {
#wrapper {
position: static;
transform: none;
margin: 0 auto;
}
}
a {
text-decoration: none;
color: white;
}

87
web/css/modal.css Normal file
View File

@@ -0,0 +1,87 @@
.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: 50vh;
left: 50vw;
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;
}
@media only screen and (max-width: 768px) {
.modal-content {
min-width: 90% !important;
margin: auto;
}
}

77
web/css/panels.css Normal file
View File

@@ -0,0 +1,77 @@
.panel-10 {
width: 10%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
margin-bottom: 30px;
}
.panel-33 {
width: 33%;
background-color: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
transition: 0.3s ease-in-out background-color;
}
.panel-75 {
width: 68%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
transition: 0.3s ease-in-out background-color;
}
.panel-90 {
width: 88%;
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;
}
@media only screen and (max-width: 768px) {
.panel-10, .panel-33, .panel-90 {
width: 90%;
margin: auto;
margin-bottom: 20px;
}
.panel-75 {
margin: 80px auto 0 auto !important;
width: 90%;
}
.panel-33 h2 {
padding: 20px;
padding-top: 5px;
}
.panel-100 {
width: 90%;
margin: auto;
}
}

View File

@@ -1,586 +0,0 @@
@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: #111;
--color-main-bright: #aaa;
--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));
}
.color-1 {
color: var(--color-1);
}
.color-2 {
color: var(--color-2);
}
.color-3 {
color: var(--color-3);
}
.color-4 {
color: var(--color-4);
}
::selection {
background: var(--color-main-bright);
color: inherit;
}
body {
font-family: 'Titillium Web', sans-serif;
color: white;
background-color: var(--color-main);
transition: 0.3s ease-in-out background-color;
}
a {
text-decoration: none;
color: white;
}
#data-pi {
text-transform: uppercase;
}
#wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: auto;
max-width: 1240px;
}
#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;
}
h3 {
font-size: 22px;
}
.flex-container {
display: flex;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.no-bg {
background: transparent !important;
}
.panel-10 {
width: 10%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
margin-bottom: 30px;
}
.panel-33 {
width: 33%;
background-color: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
transition: 0.3s ease-in-out background-color;
}
.hover-brighten:hover {
cursor: pointer;
background-color: var(--color-2);
}
.panel-75 {
width: 68%;
background: var(--color-1);
margin-left: 10px;
margin-right: 10px;
border-radius: 30px;
text-align: center;
margin-top: 30px;
transition: 0.3s ease-in-out background-color;
}
.panel-90 {
width: 88%;
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;
}
#af-list ul {
display:list-item;
padding: 0;
list-style-type: none;
height: 425px;
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: 0;
}
.auto {
margin: auto;
}
.text-big {
font-size: 60px;
font-weight: 300;
}
.text-medium {
font-size: 24px;
color: #aaa;
font-weight: 300;
}
.text-medium-big {
font-size: 46px;
}
.text-small {
font-size: 13px;
}
.text-gray {
color: #666;
}
.bg-dark {
background: #100d1f;
}
#tune-buttons input[type="text"] {
width: 50%;
height: 100%;
min-height: 46px;
padding-left: 20px;
box-sizing: border-box;
border: 2px solid transparent;
outline: 0;
color: white;
background-color: var(--color-1);
font-family: 'Titillium Web', sans-serif;
}
input[type="text"]:hover {
border: 2px solid var(--color-main-bright);
}
#tune-buttons button {
box-sizing: border-box;
background-color: var(--color-4);
border: 0;
color: var(--color-1);
width: 25%;
height: 100%;
display: block;
padding: 14px;
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
#tune-buttons button:hover {
background-color: var(--color-main-bright);
}
#freq-down {
border-radius: 30px 0 0 30px;
}
#freq-up {
border-radius: 0 30px 30px 0;
}
@media only screen and (max-width: 768px) {
.flex-container {
display: block;
}
.flex-phone {
display: flex;
}
.modal-content {
min-width: 90% !important;
margin: auto;
}
.panel-10, .panel-33, .panel-90 {
width: 90%;
margin: auto;
margin-bottom: 20px;
}
.panel-75 {
margin: 80px auto 0 auto !important;
width: 90%;
}
.panel-33 h2 {
padding: 20px;
padding-top: 5px;
}
.text-medium-big {
font-size: 32px;
}
.text-big {
font-size: 40px;
display: block;
margin-top: -25px;
}
.text-big#data-ps {
margin: 0;
}
.panel-100 {
width: 90%;
margin: auto;
}
}
@media (max-width: 960px) {
#wrapper {
position: static;
transform: none;
margin: 0 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: 50vh;
left: 50vw;
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;
}
select {
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;
font-family: inherit;
font-weight: bold;
}
select option {
font-family: 'Titillium Web', sans-serif;
font-weight: 300;
padding: 10px;
border: 0;
}
select:hover {
background: var(--color-5);
}
#data-ps, #data-rt0, #data-rt1 {
font-family: monospace;
}

View File

@@ -2,7 +2,7 @@
<html>
<head>
<title>FM-DX Webserver</title>
<link href="css/styles.css" type="text/css" rel="stylesheet">
<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>
@@ -14,7 +14,7 @@
<canvas id="signal-canvas" width="1240" height="200"></canvas>
<div class="flex-container">
<div class="panel-90 no-bg">
<div class="panel-90 bg-none">
<div class="flex-container">
<div class="panel-75 hover-brighten flex-center" id="ps-container" style="height: 110px;">
<span class="text-big" id="data-ps"></span>
@@ -38,7 +38,7 @@
<div class="flex-container">
<div class="panel-33 hover-brighten" id="pi-code-container">
<h2>PI CODE</h2>
<span id="data-pi" class="text-big"></span>
<span id="data-pi" class="text-big text-uppercase"></span>
</div>
<div class="panel-33 hover-brighten" id="freq-container">
@@ -58,12 +58,10 @@
<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 flex-container flex-phone" id="tune-buttons" style="opacity: 1;">
<!--<button id="playButton">play</button>-->
<button id="freq-down"></button>
<input type="text" id="commandinput" inputmode="numeric" placeholder="Frequency">
<button id="freq-up"></button>
@@ -81,16 +79,26 @@
</div>
</div>
<div class="panel-100">
<div class="flex-container">
<div class="panel-75 hover-brighten" id="rt-container" style="height: 110px;">
<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 class="panel-33">
<h2>
<div id="data-pty" style="color:white;"></div>
</h2>
<h3 style="margin-top:0;" class="flex-center">
</h3>
</div>
</div>
<div class="panel-10 no-bg">
</div>
<div class="panel-10 bg-none">
<div class="panel-100" style="height: 100%;">
<h2>AF</h2>
<div id="af-list" style="text-align: center;">
@@ -129,9 +137,6 @@
<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>
<script src="js/webserver.js"></script>
</body>
</html>

382
web/js/main.js Normal file
View File

@@ -0,0 +1,382 @@
$(document).ready(function() {
var hostParts = window.location.host.split(':');
var hostname = hostParts[0]; // Extract the hostname
var port = hostParts[1] || '8080'; // Extract the port or use a default (e.g., 8080)
var socketAddress = 'ws://' + hostname + ':' + port + '/text'; // Use 'wss' for secure WebSocket connections (recommended for external access)
var socket = new WebSocket(socketAddress);
var dataContainer = $('#data-container');
var canvas = $('#signal-canvas')[0];
var context = canvas.getContext('2d');
var signalToggle = $("#signal-units-toggle");
canvas.width = canvas.parentElement.clientWidth;
var data = [];
var maxDataPoints = 250;
var 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 getInitialSettings() {
$.ajax({
url: '/static_data',
dataType: 'json',
success: function(data) {
// Use the received data (data.qthLatitude, data.qthLongitude) as needed
localStorage.setItem('qthLatitude', data.qthLatitude);
localStorage.setItem('qthLongitude', data.qthLongitude);
localStorage.setItem('webServerName', data.webServerName);
document.title = 'FM-DX Webserver [' + data.webServerName + ']';
},
error: function(error) {
console.error('Error:', error);
}
});
}
getInitialSettings();
// Start updating the canvas
updateCanvas();
function updateCanvas() {
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.prop('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.html(event.data + '<br>');
};
requestAnimationFrame(updateCanvas);
}
function compareNumbers(a, b) {
return a - b;
}
function escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML.replace(' ', '&nbsp;');
}
function processString(string, errors) {
var output = '';
const max_alpha = 70;
const alpha_range = 50;
const max_error = 10;
errors = errors?.split(',');
for (let i = 0; i < string.length; i++) {
alpha = parseInt(errors[i]) * (alpha_range / (max_error + 1));
if (alpha) {
output += "<span style='opacity: " + (max_alpha - alpha) + "%'>" + escapeHTML(string[i]) + "</span>";
} else {
output += escapeHTML(string[i]);
}
}
return output;
}
function updatePanels(parsedData) {
const sortedAf = parsedData.af.sort(compareNumbers);
const scaledArray = sortedAf.map(element => element / 1000);
const listContainer = $('#af-list');
const scrollTop = listContainer.scrollTop();
let ul = listContainer.find('ul');
if (!ul.length) {
ul = $('<ul></ul>');
listContainer.append(ul);
}
ul.html('');
const listItems = scaledArray.map(element => {
return $('<li></li>').text(element.toFixed(1))[0];
});
ul.append(listItems);
listContainer.scrollTop(scrollTop);
$('#data-frequency').text(parsedData.freq);
$('#data-pi').html(parsedData.pi === '?' ? "<span class='text-gray'>?</span>" : parsedData.pi);
$('#data-ps').html(parsedData.ps === '?' ? "<span class='text-gray'>?</span>" : processString(parsedData.ps, parsedData.ps_errors));
$('#data-tp').html(parsedData.tp === false ? "<span class='text-gray'>TP</span>" : "TP");
$('#data-pty').html(europe_programmes[parsedData.pty]);
$('#data-st').html(parsedData.st === false ? "<span class='text-gray'>ST</span>" : "ST");
$('#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>');
const signalValue = signalToggle.is(':checked') ? (parsedData.signal - 11.75) : parsedData.signal;
const integerPart = Math.floor(signalValue);
const decimalPart = (signalValue - integerPart).toFixed(1).slice(1); // Adjusted this line
$('#data-signal').text(integerPart);
$('#data-signal-decimal').text(decimalPart);
$('#users-online').text(parsedData.users);
}
signalToggle.on("change", function() {
const signalText = $('#signal-units');
if (signalToggle.prop('checked')) {
signalText.text('dBµV');
} else {
signalText.text('dBf');
}
});
const textInput = $('#commandinput');
textInput.on('change', function (event) {
const inputValue = textInput.val();
// Check if the user agent contains 'iPhone'
if (/iPhone/i.test(navigator.userAgent) && socket.readyState === WebSocket.OPEN) {
socket.send(inputValue);
// Clear the input field if needed
textInput.val('');
}
});
textInput.on('keyup', function (event) {
if (event.key !== 'Backspace') {
let inputValue = textInput.val();
inputValue = inputValue.replace(/[^0-9.]/g, '');
if (inputValue.includes("..")) {
inputValue = inputValue.slice(0, inputValue.lastIndexOf('.')) + inputValue.slice(inputValue.lastIndexOf('.') + 1);
textInput.val(inputValue);
}
if (!inputValue.includes(".")) {
if (inputValue.startsWith('10') && inputValue.length > 2) {
inputValue = inputValue.slice(0, 3) + '.' + inputValue.slice(3);
textInput.val(inputValue);
} else if (inputValue.length > 2) {
inputValue = inputValue.slice(0, 2) + '.' + inputValue.slice(2);
textInput.val(inputValue);
}
}
}
if (event.key === 'Enter') {
const inputValue = textInput.val();
if (socket.readyState === WebSocket.OPEN) {
socket.send(inputValue);
}
textInput.val('');
}
});
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
getCurrentFreq();
if (socket.readyState === WebSocket.OPEN) {
if (e.keyCode == '38') {
socket.send((currentFreq + 0.01).toFixed(2));
}
else if (e.keyCode == '40') {
socket.send((currentFreq - 0.01).toFixed(2));
}
else if (e.keyCode == '37') {
socket.send((currentFreq - 0.10).toFixed(1));
}
else if (e.keyCode == '39') {
socket.send((currentFreq + 0.10).toFixed(1));
}
}
}
function getCurrentFreq() {
currentFreq = $('#data-frequency').text();
currentFreq = parseFloat(currentFreq).toFixed(3);
currentFreq = parseFloat(currentFreq);
return currentFreq;
}
var freqUpButton = $('#freq-up')[0];
var freqDownButton = $('#freq-down')[0];
var psContainer = $('#ps-container')[0];
var rtContainer = $('#rt-container')[0];
var piCodeContainer = $('#pi-code-container')[0];
var freqContainer = $('#freq-container')[0];
$(freqUpButton).on("click", tuneUp);
$(freqDownButton).on("click", tuneDown);
$(psContainer).on("click", copyPs);
$(rtContainer).on("click", copyRt);
$(piCodeContainer).on("click", findOnMaps);
$(freqContainer).on("click", function() {
textInput.focus();
});
function tuneUp() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
socket.send((currentFreq + 0.10).toFixed(1));
}
}
function tuneDown() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
socket.send((currentFreq - 0.10).toFixed(1));
}
}
async function copyPs() {
var frequency = $('#data-frequency').text();
var pi = $('#data-pi').text();
var ps = $('#data-ps').text();
var signal = $('#data-signal').text();
var signalDecimal = $('#data-signal-decimal').text();
var signalUnit = $('#signal-units').text();
try {
await copyToClipboard(frequency + " - " + pi + " | " + ps + " [" + signal + signalDecimal + " " + signalUnit + "]");
} catch(error) {
console.error(error);
}
}
async function copyRt() {
var rt0 = $('#data-rt0').text();
var rt1 = $('#data-rt1').text();
try {
await copyToClipboard("[0] RT: " + rt0 + "\n[1] RT: " + rt1);
} catch(error) {
console.error(error);
}
}
function findOnMaps() {
var frequency = $('#data-frequency').text();
var pi = $('#data-pi').text();
var latitude = localStorage.getItem('qthLongitude');
var longitude = localStorage.getItem('qthLatitude');
frequency = parseFloat(frequency).toFixed(1);
var url = "https://maps.fmdx.pl/#qth=" + longitude + "," + latitude + "&freq=" + frequency + "&pi=" + pi;
window.open(url, "_blank");
}
function copyToClipboard(textToCopy) {
// Navigator clipboard api needs a secure context (https)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy)
.catch(function(err) {
console.error('Error:', err);
});
} else {
var textArea = $('<textarea></textarea>');
textArea.val(textToCopy);
textArea.css({
'position': 'absolute',
'left': '-999999px'
});
$('body').prepend(textArea);
textArea.select();
try {
document.execCommand('copy');
} catch (error) {
console.error('Error:', error);
} finally {
textArea.remove();
}
}
}
});

33
web/js/modal.js Normal file
View File

@@ -0,0 +1,33 @@
$(document).ready(function() {
// Cache jQuery objects for reuse
var modal = $("#myModal");
var openBtn = $("#settings");
var closeBtn = $("#closeModal, #closeModalButton");
// Function to open the modal
function openModal() {
modal.css("display", "block");
setTimeout(function() {
modal.css("opacity", 1);
}, 10);
}
// Function to close the modal
function closeModal() {
modal.css("opacity", 0);
setTimeout(function() {
modal.css("display", "none");
}, 300);
}
// Event listeners for the open and close buttons
openBtn.on("click", openModal);
closeBtn.on("click", closeModal);
// Close the modal when clicking outside of it
$(document).on("click", function(event) {
if ($(event.target).is(modal)) {
closeModal();
}
});
});

33
web/js/themes.js Normal file
View File

@@ -0,0 +1,33 @@
const themes = {
theme1: ['#1d1838', '#8069fa'],
theme2: ['#381818', '#ff7070'],
theme3: ['#121c0c', '#a9ff70'],
theme4: ['#0c1c1b', '#68f7ee'],
theme5: ['#171106', '#f5b642'],
theme6: ['#21091d', '#ed51d3'],
theme7: ['#111', '#aaa']
};
function setTheme(themeName) {
const themeColors = themes[themeName];
if (themeColors) {
$(':root').css('--color-main', themeColors[0]);
$(':root').css('--color-main-bright', themeColors[1]);
}
}
$(document).ready(() => {
const themeSelector = $('#theme-selector');
const savedTheme = localStorage.getItem('theme');
if (savedTheme && themes[savedTheme]) {
setTheme(savedTheme);
themeSelector.val(savedTheme);
}
themeSelector.on('change', (event) => {
const selectedTheme = event.target.value;
setTheme(selectedTheme);
localStorage.setItem('theme', selectedTheme);
});
});

3
web/js/webserver.js Normal file
View File

@@ -0,0 +1,3 @@
$.getScript('/js/main.js');
$.getScript('/js/modal.js');
$.getScript('/js/themes.js');

View File

@@ -1,406 +0,0 @@
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 getInitialSettings() {
fetch('/static_data')
.then(response => response.json())
.then(data => {
// Use the received data (data.qthLatitude, data.qthLongitude) as needed
localStorage.setItem('qthLatitude', data.qthLatitude);
localStorage.setItem('qthLongitude', data.qthLongitude);
localStorage.setItem('webServerName', data.webServerName);
document.title = 'FM-DX Webserver [' + data.webServerName + ']';
})
.catch(error => console.error('Error:', error));
}
getInitialSettings();
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 escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML.replace(' ', '&nbsp;');
}
function processString(string, errors) {
var output = '';
const max_alpha = 70;
const alpha_range = 50;
const max_error = 10;
errors = errors.split(',');
for (let i = 0; i < string.length; i++) {
alpha = parseInt(errors[i]) * (alpha_range / (max_error + 1));
if (alpha) {
output += "<span style='opacity: " + (max_alpha - alpha) + "%'>" + escapeHTML(string[i]) + "</span>";
} else {
output += escapeHTML(string[i]);
}
}
return output;
}
function updatePanels(parsedData) {
// Assuming sortedAf is your array
const sortedAf = parsedData.af.sort(compareNumbers);
// Convert the values in the array (dividing by 1000)
const scaledArray = sortedAf.map(element => element / 1000);
// Get the container element where you want to display the list
const listContainer = document.querySelector('#af-list');
// Preserve the current scroll position
const scrollTop = listContainer.scrollTop;
// Get the existing ul element
const ul = listContainer.querySelector('ul');
// If ul doesn't exist, create a new one
if (!ul) {
ul = document.createElement('ul');
listContainer.appendChild(ul);
}
// Remove existing list items
ul.innerHTML = '';
// Create an array of list items
const listItems = scaledArray.map(element => {
const li = document.createElement('li');
li.textContent = element.toFixed(1);
return li;
});
// Append the list items to the unordered list
listItems.forEach(li => ul.appendChild(li));
// Restore the scroll position
listContainer.scrollTop = scrollTop;
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>" : processString(parsedData.ps, parsedData.ps_errors);
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 = processString(parsedData.rt0, parsedData.rt0_errors);
document.querySelector('#data-rt1').innerHTML = processString(parsedData.rt1, parsedData.rt1_errors);
document.querySelector('#data-flag').innerHTML = '<i title="' + parsedData.country_name + '" class="flag-sm flag-sm-' + parsedData.country_iso + '"></i>';
const signalValue = signalToggle.checked ? (parsedData.signal - 11.75) : parsedData.signal;
const integerPart = Math.floor(signalValue);
const decimalPart = (signalValue - integerPart).toFixed(1).slice(1); // Adjusted this line
document.querySelector('#data-signal').textContent = integerPart;
document.querySelector('#data-signal-decimal').textContent = decimalPart;
document.querySelector('#users-online').textContent = parsedData.users;
}
signalToggle.addEventListener("change", function() {
signalText = document.querySelector('#signal-units');
if (signalToggle.checked) {
signalText.textContent = 'dBµV';
} else {
// Checkbox is unchecked
signalText.textContent = 'dBf';
}
});
const textInput = document.getElementById('commandinput');
textInput.addEventListener('change', function (event) {
const inputValue = textInput.value;
// Check if the user agent contains 'iPhone'
if (/iPhone/i.test(navigator.userAgent) && socket.readyState === WebSocket.OPEN) {
socket.send(inputValue);
// Clear the input field if needed
textInput.value = '';
}
});
textInput.addEventListener('keyup', function (event) {
// Check if the pressed key is 'Backspace' (key code 8)
if (event.key !== 'Backspace') {
// Get the current input value
let inputValue = textInput.value;
// Remove non-digit characters (excluding dot)
inputValue = inputValue.replace(/[^0-9.]/g, '');
// Remove the last dot if there are two consecutive dots
if (inputValue.includes("..")) {
inputValue = inputValue.slice(0, inputValue.lastIndexOf('.')) + inputValue.slice(inputValue.lastIndexOf('.') + 1);
textInput.value = inputValue;
}
// Determine where to add the dot based on the frequency range
if (!inputValue.includes(".")) {
if (inputValue.startsWith('10') && inputValue.length > 2) {
// For frequencies starting with '10', add the dot after the third digit
inputValue = inputValue.slice(0, 3) + '.' + inputValue.slice(3);
textInput.value = inputValue;
} else if (inputValue.length > 2) {
// For other frequencies, add the dot after the second digit
inputValue = inputValue.slice(0, 2) + '.' + inputValue.slice(2);
textInput.value = inputValue;
}
}
}
// Update the input value
// Check if the pressed key is 'Enter' (key code 13)
if (event.key === 'Enter') {
// Retrieve the input value
const inputValue = textInput.value;
// Send the input value to the WebSocket
if (socket.readyState === WebSocket.OPEN) {
socket.send(inputValue);
}
// Clear the input field if needed
textInput.value = '';
}
});
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
getCurrentFreq();
if (socket.readyState === WebSocket.OPEN) {
if (e.keyCode == '38') {
socket.send((currentFreq + 0.01).toFixed(2));
}
else if (e.keyCode == '40') {
socket.send((currentFreq - 0.01).toFixed(2));
}
else if (e.keyCode == '37') {
socket.send((currentFreq - 0.10).toFixed(1));
}
else if (e.keyCode == '39') {
socket.send((currentFreq + 0.10).toFixed(1));
}
}
}
function getCurrentFreq() {
currentFreq = document.getElementById("data-frequency").textContent;
currentFreq = parseFloat(currentFreq).toFixed(3);
currentFreq = parseFloat(currentFreq);
return currentFreq;
}
freqUpButton = document.getElementById('freq-up');
freqDownButton = document.getElementById('freq-down');
psContainer = document.getElementById('ps-container');
piCodeContainer = document.getElementById('pi-code-container');
freqContainer = document.getElementById('freq-container');
freqUpButton.addEventListener("click", tuneUp);
freqDownButton.addEventListener("click", tuneDown);
psContainer.addEventListener("click", copyPs);
piCodeContainer.addEventListener("click", findOnMaps);
freqContainer.addEventListener("click", function() {
textInput.focus();
});
function tuneUp() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
socket.send((currentFreq + 0.10).toFixed(1));
}
}
function tuneDown() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
socket.send((currentFreq - 0.10).toFixed(1));
}
}
async function copyPs() {
let frequency = document.querySelector('#data-frequency').textContent;
let pi = document.querySelector('#data-pi').textContent;
let ps = document.querySelector('#data-ps').textContent;
let signal = document.querySelector('#data-signal').textContent;
let signalDecimal = document.querySelector('#data-signal-decimal').textContent;
let signalUnit = document.querySelector('#signal-units').textContent;
try {
await copyToClipboard(frequency + " - " + pi + " | " + ps + " [" + signal + signalDecimal + " " + signalUnit + "]");
} catch(error) {
console.error(error);
}
}
function findOnMaps() {
let frequency = document.querySelector('#data-frequency').textContent;
let pi = document.querySelector('#data-pi').textContent;
let latitude = localStorage.getItem('qthLongitude');
let longitude = localStorage.getItem('qthLatitude');
frequency = parseFloat(frequency).toFixed(1);
window.open("https://maps.fmdx.pl/#qth=" + longitude + "," + latitude + "&freq=" + frequency + "&pi=" + pi, "_blank");
}
async function copyToClipboard(textToCopy) {
// Navigator clipboard api needs a secure context (https)
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(textToCopy);
} else {
const textArea = document.createElement("textarea");
textArea.value = textToCopy;
textArea.style.position = "absolute";
textArea.style.left = "-999999px";
document.body.prepend(textArea);
textArea.select();
try {
document.execCommand('copy');
} catch (error) {
console.error(error);
} finally {
textArea.remove();
}
}
}

View File

@@ -1,33 +0,0 @@
// 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();
}
});

View File

@@ -1,20 +0,0 @@
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;
});

View File

@@ -1,57 +0,0 @@
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',
},
theme6: {
'--color-main': '#21091d',
'--color-main-bright': '#ed51d3',
},
theme7: {
'--color-main': '#111',
'--color-main-bright': '#aaa',
}
};
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);
});