You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 22:13:53 +01:00
refactor
This commit is contained in:
17
console.js
Normal file
17
console.js
Normal 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
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
35
index.js
35
index.js
@@ -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 });
|
||||
});
|
||||
@@ -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
47
web/css/breadcrumbs.css
Normal 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
241
web/css/buttons.css
Normal 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
7
web/css/entry.css
Normal 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
89
web/css/helpers.css
Normal 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
50
web/css/main.css
Normal 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
87
web/css/modal.css
Normal 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
77
web/css/panels.css
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
382
web/js/main.js
Normal 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(' ', ' ');
|
||||
}
|
||||
|
||||
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
33
web/js/modal.js
Normal 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
33
web/js/themes.js
Normal 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
3
web/js/webserver.js
Normal file
@@ -0,0 +1,3 @@
|
||||
$.getScript('/js/main.js');
|
||||
$.getScript('/js/modal.js');
|
||||
$.getScript('/js/themes.js');
|
||||
406
web/main.js
406
web/main.js
@@ -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(' ', ' ');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
web/modal.js
33
web/modal.js
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user