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
css UI fixes, new panel, code optimizaiton, security fixes
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "fm-dx-webserver",
|
"name": "fm-dx-webserver",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fm-dx-webserver",
|
"name": "fm-dx-webserver",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/node-pre-gyp": "1.0.11",
|
"@mapbox/node-pre-gyp": "1.0.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fm-dx-webserver",
|
"name": "fm-dx-webserver",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"description": "FM DX Webserver",
|
"description": "FM DX Webserver",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ function handleData(wss, receivedData, rdsWss) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError("Error fetching Tx info:", error);
|
console.log("Error fetching Tx info:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send the updated data to the client
|
// Send the updated data to the client
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const dataHandler = require('./datahandler');
|
const dataHandler = require('./datahandler');
|
||||||
@@ -56,44 +57,86 @@ function authenticateWithXdrd(client, salt, password) {
|
|||||||
client.write('x\n');
|
client.write('x\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnect(clientIp, currentUsers, ws) {
|
const ipCache = new Map();
|
||||||
http.get(`http://ip-api.com/json/${clientIp}`, (response) => {
|
|
||||||
let data = '';
|
|
||||||
|
|
||||||
response.on('data', (chunk) => {
|
function handleConnect(clientIp, currentUsers, ws, callback) {
|
||||||
data += chunk;
|
if (ipCache.has(clientIp)) {
|
||||||
});
|
// Use cached location info
|
||||||
|
processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback);
|
||||||
response.on('end', () => {
|
|
||||||
try {
|
|
||||||
const locationInfo = JSON.parse(data);
|
|
||||||
const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
||||||
const connectionTime = new Date().toLocaleString([], options);
|
|
||||||
|
|
||||||
if (locationInfo.as?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(locationInfo.country === undefined) {
|
http.get(`http://ip-api.com/json/${clientIp}`, (response) => {
|
||||||
const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
|
let data = "";
|
||||||
storage.connectedUsers.push(userData);
|
|
||||||
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
|
response.on("data", (chunk) => {
|
||||||
} else {
|
data += chunk;
|
||||||
const userLocation = `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
|
});
|
||||||
const userData = { ip: clientIp, location: userLocation, time: connectionTime, instance: ws };
|
|
||||||
storage.connectedUsers.push(userData);
|
response.on("end", () => {
|
||||||
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.country}`);
|
try {
|
||||||
}
|
const locationInfo = JSON.parse(data);
|
||||||
|
ipCache.set(clientIp, locationInfo); // Store in cache
|
||||||
|
processConnection(clientIp, locationInfo, currentUsers, ws, callback);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error("Error parsing location data:", error);
|
||||||
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
|
callback("User allowed");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).on('error', (err) => {
|
}).on("error", (err) => {
|
||||||
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
|
console.error("Error fetching location data:", err);
|
||||||
|
callback("User allowed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||||
|
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
|
||||||
|
const connectionTime = new Date().toLocaleString([], options);
|
||||||
|
|
||||||
|
https.get("https://fmdx.org/banned_as.json", (banResponse) => {
|
||||||
|
let banData = "";
|
||||||
|
|
||||||
|
banResponse.on("data", (chunk) => {
|
||||||
|
banData += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
banResponse.on("end", () => {
|
||||||
|
try {
|
||||||
|
const bannedAS = JSON.parse(banData).banned_as || [];
|
||||||
|
|
||||||
|
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
|
||||||
|
return callback("User banned");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLocation =
|
||||||
|
locationInfo.country === undefined
|
||||||
|
? "Unknown"
|
||||||
|
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
|
||||||
|
|
||||||
|
storage.connectedUsers.push({
|
||||||
|
ip: clientIp,
|
||||||
|
location: userLocation,
|
||||||
|
time: connectionTime,
|
||||||
|
instance: ws,
|
||||||
|
});
|
||||||
|
|
||||||
|
consoleCmd.logInfo(
|
||||||
|
`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
|
||||||
|
);
|
||||||
|
|
||||||
|
callback("User allowed");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing banned AS list:", error);
|
||||||
|
callback("User allowed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on("error", (err) => {
|
||||||
|
console.error("Error fetching banned AS list:", err);
|
||||||
|
callback("User allowed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function formatUptime(uptimeInSeconds) {
|
function formatUptime(uptimeInSeconds) {
|
||||||
const secondsInMinute = 60;
|
const secondsInMinute = 60;
|
||||||
const secondsInHour = secondsInMinute * 60;
|
const secondsInHour = secondsInMinute * 60;
|
||||||
|
|||||||
126
server/index.js
126
server/index.js
@@ -326,73 +326,16 @@ app.set('view engine', 'ejs');
|
|||||||
app.set('views', path.join(__dirname, '../web'));
|
app.set('views', path.join(__dirname, '../web'));
|
||||||
app.use('/', endpoints);
|
app.use('/', endpoints);
|
||||||
|
|
||||||
// Anti-spam function
|
|
||||||
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
|
|
||||||
const command = message.toString();
|
|
||||||
const now = Date.now();
|
|
||||||
if (endpointName === 'text') logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
|
|
||||||
|
|
||||||
// Initialize user command history if not present
|
|
||||||
if (!userCommandHistory[clientIp]) {
|
|
||||||
userCommandHistory[clientIp] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the current timestamp for the user
|
|
||||||
userCommandHistory[clientIp].push(now);
|
|
||||||
|
|
||||||
// Remove timestamps older than 20 ms from the history
|
|
||||||
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
|
|
||||||
|
|
||||||
// Check if there are 8 or more commands in the last 20 ms
|
|
||||||
if (userCommandHistory[clientIp].length >= 8) {
|
|
||||||
logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
|
|
||||||
|
|
||||||
// Add to banlist if not already banned
|
|
||||||
if (!serverConfig.webserver.banlist.includes(clientIp)) {
|
|
||||||
serverConfig.webserver.banlist.push(clientIp);
|
|
||||||
logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
|
|
||||||
console.log(serverConfig.webserver.banlist);
|
|
||||||
configSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.close(1008, 'Bot-like behavior detected');
|
|
||||||
return command; // Return command value before closing connection
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the last message time for general spam detection
|
|
||||||
lastMessageTime = now;
|
|
||||||
|
|
||||||
// Initialize command history for rate-limiting checks
|
|
||||||
if (!userCommands[command]) {
|
|
||||||
userCommands[command] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the current timestamp for this command
|
|
||||||
userCommands[command].push(now);
|
|
||||||
|
|
||||||
// Remove timestamps older than 1 second
|
|
||||||
userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000);
|
|
||||||
|
|
||||||
// If command count exceeds limit, close connection
|
|
||||||
if (userCommands[command].length > lengthCommands) {
|
|
||||||
if (now - lastWarn.time > 1000) { // Check if 1 second has passed
|
|
||||||
logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`);
|
|
||||||
lastWarn.time = now; // Update the last warning time
|
|
||||||
}
|
|
||||||
ws.close(1008, 'Spamming detected');
|
|
||||||
return command; // Return command value before closing connection
|
|
||||||
}
|
|
||||||
|
|
||||||
return command; // Return command value for normal execution
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WEBSOCKET BLOCK
|
* WEBSOCKET BLOCK
|
||||||
*/
|
*/
|
||||||
|
const tunerLockTracker = new WeakMap();
|
||||||
|
|
||||||
wss.on('connection', (ws, request) => {
|
wss.on('connection', (ws, request) => {
|
||||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
||||||
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||||
const userCommandHistory = {};
|
const userCommandHistory = {};
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
return;
|
return;
|
||||||
@@ -402,19 +345,25 @@ wss.on('connection', (ws, request) => {
|
|||||||
clientIp = clientIp.split(',')[0].trim();
|
clientIp = clientIp.split(',')[0].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||||
|
(request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') ||
|
||||||
|
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||||
currentUsers++;
|
currentUsers++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
helpers.handleConnect(clientIp, currentUsers, ws, (result) => {
|
||||||
|
if (result === "User banned") {
|
||||||
|
ws.close(1008, 'Banned IP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dataHandler.showOnlineUsers(currentUsers);
|
dataHandler.showOnlineUsers(currentUsers);
|
||||||
|
|
||||||
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
|
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
|
||||||
serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n');
|
serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
helpers.handleConnect(clientIp, currentUsers, ws);
|
|
||||||
|
|
||||||
// Anti-spam tracking for each client
|
|
||||||
const userCommands = {};
|
const userCommands = {};
|
||||||
let lastWarn = { time: 0 };
|
let lastWarn = { time: 0 };
|
||||||
|
|
||||||
@@ -431,13 +380,26 @@ wss.on('connection', (ws, request) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.startsWith('w') && request.session.isAdminAuthenticated) {
|
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
|
||||||
|
|
||||||
|
if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'wL1': serverConfig.lockToAdmin = true; break;
|
case 'wL1':
|
||||||
case 'wL0': serverConfig.lockToAdmin = false; break;
|
if (isAdminAuthenticated) serverConfig.lockToAdmin = true;
|
||||||
case 'wT0': serverConfig.publicTuner = true; break;
|
break;
|
||||||
case 'wT1': serverConfig.publicTuner = false; break;
|
case 'wL0':
|
||||||
default: break;
|
if (isAdminAuthenticated) serverConfig.lockToAdmin = false;
|
||||||
|
break;
|
||||||
|
case 'wT0':
|
||||||
|
serverConfig.publicTuner = true;
|
||||||
|
if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
|
||||||
|
break;
|
||||||
|
case 'wT1':
|
||||||
|
serverConfig.publicTuner = false;
|
||||||
|
if(!isAdminAuthenticated) tunerLockTracker.set(ws, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,16 +412,15 @@ wss.on('connection', (ws, request) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
|
|
||||||
|
|
||||||
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
|
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
|
||||||
output.write(`${command}\n`);
|
output.write(`${command}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', (code, reason) => {
|
ws.on('close', (code, reason) => {
|
||||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||||
|
(request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') ||
|
||||||
|
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||||
currentUsers--;
|
currentUsers--;
|
||||||
}
|
}
|
||||||
dataHandler.showOnlineUsers(currentUsers);
|
dataHandler.showOnlineUsers(currentUsers);
|
||||||
@@ -473,7 +434,15 @@ wss.on('connection', (ws, request) => {
|
|||||||
storage.connectedUsers = [];
|
storage.connectedUsers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
|
if (tunerLockTracker.has(ws)) {
|
||||||
|
logInfo(`User who locked the tuner left. Unlocking the tuner.`);
|
||||||
|
output.write('wT0\n')
|
||||||
|
tunerLockTracker.delete(ws);
|
||||||
|
serverConfig.publicTuner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUsers === 0 && serverConfig.enableDefaultFreq === true &&
|
||||||
|
serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (currentUsers === 0) {
|
if (currentUsers === 0) {
|
||||||
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
|
output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n');
|
||||||
@@ -481,20 +450,21 @@ wss.on('connection', (ws, request) => {
|
|||||||
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||||
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3);
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
|
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
|
||||||
client.write('X\n');
|
client.write('X\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (code !== 1008) {
|
||||||
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
|
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('error', console.error);
|
ws.on('error', console.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// CHAT WEBSOCKET BLOCK
|
// CHAT WEBSOCKET BLOCK
|
||||||
chatWss.on('connection', (ws, request) => {
|
chatWss.on('connection', (ws, request) => {
|
||||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||||
|
|||||||
@@ -14,9 +14,16 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1#tuner-name {
|
h1#tuner-name {
|
||||||
font-size: 32px;
|
font-size: 26px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
text-transform: initial;
|
text-transform: initial;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s ease color;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1#tuner-name:hover {
|
||||||
|
color: var(--color-main-bright);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -43,7 +50,8 @@ h4 {
|
|||||||
|
|
||||||
.tooltiptext {
|
.tooltiptext {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: var(--color-3);
|
background-color: var(--color-2);
|
||||||
|
border: 2px solid var(--color-3);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -54,7 +62,7 @@ h4 {
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
p#tuner-desc {
|
p.tuner-desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,61 +124,63 @@ table .form-group {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settings, #back-btn, #users-online-container {
|
.hidden-panel {
|
||||||
background: transparent;
|
display: none;
|
||||||
border: 0;
|
|
||||||
color: var(--color-text);
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15px;
|
left: 0;
|
||||||
right: 15px;
|
top: 100%;
|
||||||
font-size: 16px;
|
width: 100%;
|
||||||
width: 64px;
|
max-width: 1160px;
|
||||||
height: 64px;
|
background: var(--color-1-transparent);
|
||||||
line-height: 64px;
|
color: white;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
border-radius: 50%;
|
padding: 20px;
|
||||||
transition: 500ms ease background;
|
backdrop-filter: blur(5px);
|
||||||
cursor: pointer;
|
z-index: 10;
|
||||||
|
border-radius: 0 0 15px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatbutton.hide-desktop {
|
#settings, #users-online-container, .chatbutton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: var(--color-text);
|
color: var(--color-4);
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 15px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 64px;
|
width: 48px;
|
||||||
height: 64px;
|
height: 48px;
|
||||||
line-height: 64px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 50%;
|
border-radius: 15px;
|
||||||
transition: 500ms ease background;
|
transition: 300ms ease background;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#settings:hover, #back-btn:hover, #users-online-container:hover {
|
#users-online-container {
|
||||||
background: var(--color-3);
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatbutton, #settings {
|
||||||
|
background-color: var(--color-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings:hover, #users-online-container:hover, .chatbutton:hover {
|
||||||
|
background: var(--color-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#users-online-container {
|
#users-online-container {
|
||||||
top: 80px;
|
top: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#back-btn {
|
#af-list {
|
||||||
left: 15px;
|
overflow-y: auto;
|
||||||
right: auto;
|
max-height: 345px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#af-list ul {
|
#af-list ul {
|
||||||
display: list-item;
|
display: list-item;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin-bottom: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
max-height: 380px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#af-list a {
|
#af-list a {
|
||||||
@@ -321,23 +331,28 @@ pre {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-200-px {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
canvas, #flags-container {
|
canvas, #flags-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#tuner-desc {
|
.tuner-desc {
|
||||||
margin-bottom: 20px !important;
|
margin-bottom: 20px !important;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
#ps-container {
|
#ps-container {
|
||||||
background-color: var(--color-1-transparent);
|
background-color: var(--color-1-transparent);
|
||||||
height: 100px !important;
|
height: 100px !important;
|
||||||
margin: auto !important;
|
margin: auto !important;
|
||||||
|
margin-top: 30px !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
h1#tuner-name {
|
|
||||||
max-width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
h2 {
|
h2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -364,7 +379,7 @@ pre {
|
|||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
}
|
}
|
||||||
#data-frequency {
|
#data-frequency {
|
||||||
font-size: 64px;
|
font-size: 58px;
|
||||||
}
|
}
|
||||||
#data-rt0, #data-rt1 {
|
#data-rt0, #data-rt1 {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -426,7 +441,7 @@ pre {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 769px) and (max-height: 860px) {
|
@media only screen and (min-width: 769px) and (max-height: 720px) {
|
||||||
#rt-container {
|
#rt-container {
|
||||||
height: 90px !important;
|
height: 90px !important;
|
||||||
}
|
}
|
||||||
@@ -447,10 +462,6 @@ pre {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tuner-info #tuner-desc {
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -468,11 +479,10 @@ pre {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top: 2px !important;
|
margin-top: 2px !important;
|
||||||
}
|
}
|
||||||
#af-list ul {
|
|
||||||
height: 225px !important;
|
#af-list {
|
||||||
}
|
overflow-y: auto;
|
||||||
.chatbutton {
|
max-height: 330px;
|
||||||
height: 88px !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,11 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.m-10 {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.m-left-20 {
|
.m-left-20 {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
@@ -186,6 +191,10 @@
|
|||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottom-10 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-20 {
|
.bottom-20 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@@ -238,12 +247,34 @@ table .input-text {
|
|||||||
animation: blinker 1.5s infinite;
|
animation: blinker 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollable-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto; /* Enables horizontal scrolling */
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none; /* Hide scrollbar in Firefox */
|
||||||
|
-ms-overflow-style: none; /* Hide scrollbar in Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari */
|
||||||
|
.scrollable-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chevron styling */
|
||||||
|
.scroll-left,
|
||||||
|
.scroll-right {
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
cursor: pointer;
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes blinker {
|
@keyframes blinker {
|
||||||
0% {
|
0% {
|
||||||
background-color: var(--color-4);
|
background-color: var(--color-3);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
background-color: var(--color-2);
|
background-color: var(--color-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +317,7 @@ table .input-text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Laptop compact view */
|
/* Laptop compact view */
|
||||||
@media only screen and (min-width: 960px) and (max-height: 860px) {
|
@media only screen and (min-width: 960px) and (max-height: 720px) {
|
||||||
.text-big {
|
.text-big {
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,13 +60,21 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wrapper-outer {
|
.wrapper-outer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
background-color: var(--color-main);
|
background-color: var(--color-main);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper-outer:not(.dashboard-panel) {
|
||||||
|
min-height: calc(100vh - 84px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper-outer.wrapper-full {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +87,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc(0% + 1180px);
|
max-width: calc(0% + 1180px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#wrapper.setup-wrapper {
|
#wrapper.setup-wrapper {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: static;
|
position: static;
|
||||||
@@ -135,15 +144,6 @@ hr {
|
|||||||
border-radius: 0px 15px 15px 0px;
|
border-radius: 0px 15px 15px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
|
||||||
#wrapper {
|
|
||||||
position: static;
|
|
||||||
transform: none;
|
|
||||||
margin: 50px auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#wrapper.setup-wrapper {
|
#wrapper.setup-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -107,11 +107,6 @@ body.modal-open {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-panel-content .version-info {
|
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-panel-footer {
|
.modal-panel-footer {
|
||||||
width: 450px;
|
width: 450px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
@@ -171,7 +166,7 @@ body.modal-open {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.modal-panel-chat {
|
.modal-panel-chat {
|
||||||
height: 550px;
|
height: 510px;
|
||||||
}
|
}
|
||||||
#chat-chatbox {
|
#chat-chatbox {
|
||||||
height: 333px !important;
|
height: 333px !important;
|
||||||
|
|||||||
@@ -93,21 +93,13 @@
|
|||||||
.panel-90 {
|
.panel-90 {
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
}
|
}
|
||||||
|
.panel-100-real.bg-phone {
|
||||||
|
background-color: var(--color-1-transparent);
|
||||||
|
backdrop-filteR: blur(5px) !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
}
|
}
|
||||||
|
#dashboard-panel-description {
|
||||||
/* Laptop compact view */
|
backdrop-filter: blur(25px) !important;
|
||||||
@media only screen and (min-width: 960px) and (max-height: 860px) {
|
|
||||||
*[class^="panel-"] {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.panel-90 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.panel-10 {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
.panel-10.hide-phone {
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
#toast-container {
|
#toast-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 96px;
|
right: 32px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
186
web/index.ejs
186
web/index.ejs
@@ -36,22 +36,116 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="wrapper-outer">
|
<div class="wrapper-outer dashboard-panel" style="padding-top: 20px; z-index: 10; position: relative;">
|
||||||
<div id="wrapper">
|
<div class="panel-100-real m-0 flex-container bg-phone flex-phone-column" style="min-height: 64px; max-width: 1160px; margin-top: 10px;align-items: center; justify-content: space-between; padding-left: 20px;padding-right: 10px;">
|
||||||
<div class="panel-100 no-bg tuner-info">
|
<h1 id="tuner-name" class="text-left flex-container flex-phone flex-center" style="padding-bottom: 3px; padding-right: 5px;height: 64px;">
|
||||||
<h1 id="tuner-name"><span class="text-bold" style="color: var(--color-text);">[</span> <%= tunerName %> <span class="text-bold" style="color: var(--color-text);">]</span>
|
<span class="text-200-px" style="max-width: 450px;"><%= tunerName %></span> <i class="fa-solid fa-chevron-down p-left-10" style="font-size: 15px;"></i>
|
||||||
<% if (!publicTuner) { %><i class="fa-solid fa-key pointer tooltip" aria-label="Only people with tune password can tune." data-tooltip="Only people with tune password can tune."></i>
|
|
||||||
<% } if (tunerLock) { %><i class="fa-solid fa-lock pointer tooltip" aria-label="Tuner is currently locked to admin." data-tooltip="Tuner is currently locked to admin."></i>
|
|
||||||
<% } %>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p id="tuner-desc">
|
<% if(!publicTuner || tunerLock) { %>
|
||||||
|
<div class="tuner-status p-10 color-3">
|
||||||
|
<% if (!publicTuner) { %><i class="fa-solid fa-key pointer tooltip fa-lg" aria-label="Only people with tune password can tune." data-tooltip="Only people with tune password can tune."></i>
|
||||||
|
<% } if (tunerLock) { %><i class="fa-solid fa-lock pointer tooltip fa-lg" aria-label="Tuner is currently locked to admin." data-tooltip="Tuner is currently locked to admin."></i>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<div class="flex-container flex-phone" style="flex: 1;overflow-x: auto;align-items: center;">
|
||||||
|
<div class="plugin-list scroll-left">
|
||||||
|
<i class="fa-solid fa-chevron-left fa-lg color-4"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-panel-plugin-list flex-container hide-phone" style="flex: 1;overflow-x: auto;">
|
||||||
|
<div class="flex-container scrollable-container"></div>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-list scroll-right">
|
||||||
|
<i class="fa-solid fa-chevron-right fa-lg color-4"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-left: auto;" class="dashboard-panel-plugin-content"></div>
|
||||||
|
<div>
|
||||||
|
<button id="users-online-container" class="hide-phone" aria-label="Online users"><i class="fa-solid fa-user"></i> <span class="users-online"></span></button>
|
||||||
|
<% if (chatEnabled) { %>
|
||||||
|
<button class="chatbutton hide-phone" aria-label="Chatbox"><i class="fa-solid fa-comments"></i></button>
|
||||||
|
<% } %>
|
||||||
|
<button id="settings" aria-label="Settings" class="hide-phone"><i class="fa-solid fa-gear"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="dashboard-panel-description" class="hidden-panel">
|
||||||
|
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="tuner-desc">
|
||||||
<%- tunerDesc %>
|
<%- tunerDesc %>
|
||||||
<% if(tuningLimit && tuningLimit == true){ %>
|
<% if(tuningLimit && tuningLimit == true){ %>
|
||||||
<br><span class="text-small">Limit: <span class="color-4"><%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz</span></span><br>
|
<br><span class="text-small">Limit: <span class="color-4"><%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz</span></span><br>
|
||||||
<% } %>
|
<% } %>
|
||||||
</p>
|
|
||||||
<div style="clear: both"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-phone" style="margin-left: auto;flex-direction: row-reverse;justify-content: space-around;">
|
||||||
|
<div class="flex-container flex-phone" style="text-align: right;justify-content: right;align-items: center; height: 64px;">
|
||||||
|
<div>
|
||||||
|
<span class="">Device</span><br>
|
||||||
|
<span class="text-small color-4">
|
||||||
|
<% if (device == 'tef') { %>TEF668x<% } %>
|
||||||
|
<% if (device == 'xdr') { %>Sony XDR<% } %>
|
||||||
|
<% if (device == 'sdr') { %>SDR<% } %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="color-3 m-10 text-medium">
|
||||||
|
<i class="fa-solid fa-fw fa-radio"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-container flex-phone" style="text-align: right;justify-content: right;align-items: center; height: 64px;">
|
||||||
|
<div>
|
||||||
|
<span class="">Server time</span><br>
|
||||||
|
<span class="text-small color-4" id="server-time"></span>
|
||||||
|
</div>
|
||||||
|
<div class="color-3 m-10 text-medium">
|
||||||
|
<i class="fa-solid fa-fw fa-stopwatch"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width: 1px;background: var(--color-2);" class="m-10 hide-phone"></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style="height: 64px;" class="flex-center flex-phone">
|
||||||
|
<button class="no-bg color-4 hover-brighten" id="preset1" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||||
|
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||||
|
<span style="font-size: 10px; color: var(--color-text);" id="preset1-text"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="no-bg color-4 hover-brighten" id="preset2" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||||
|
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||||
|
<span style="font-size: 10px; color: var(--color-text);" id="preset2-text"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="no-bg color-4 hover-brighten" id="preset3" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||||
|
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||||
|
<span style="font-size: 10px; color: var(--color-text);" id="preset3-text"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="no-bg color-4 hover-brighten" id="preset4" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||||
|
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||||
|
<span style="font-size: 10px; color: var(--color-text);" id="preset4-text"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-container flex-phone" style="align-items: center; height: 64px;">
|
||||||
|
<div class="color-3 m-10 text-medium">
|
||||||
|
<i class="fa-solid fa-fw fa-user-tie"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="">Owner contact</span><br>
|
||||||
|
<span class="text-small color-4 text-200-px tooltip" data-tooltip="<%= ownerContact %>" data-tooltip-placement="bottom"><%= ownerContact %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wrapper-outer" style="z-index: 100;">
|
||||||
|
<div id="wrapper">
|
||||||
<div class="canvas-container hide-phone">
|
<div class="canvas-container hide-phone">
|
||||||
<canvas id="signal-canvas"></canvas>
|
<canvas id="signal-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,23 +369,14 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-10 no-bg m-0">
|
<div class="panel-10 no-bg" style="margin-left: 0; margin-top: 0; margin-right: 0;display:flex;">
|
||||||
<div class="panel-100 w-100" style="margin-left: 0;">
|
<div class="panel-100" style="margin-left: 0;">
|
||||||
<h2>AF</h2>
|
<h2 class="bottom-10">AF</h2>
|
||||||
<div id="af-list" class="p-bottom-20" style="text-align: center;">
|
<div id="af-list" style="text-align: center;">
|
||||||
<% if (chatEnabled) { %><ul style="height: 231px;">
|
<ul> </ul>
|
||||||
<% } else { %>
|
</div>
|
||||||
<ul style="height: 351px;">
|
</div>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% if (chatEnabled) { %>
|
|
||||||
<div class="panel-10 no-bg h-100 hide-phone" style="width: 100px; height: 100px; margin-left: 0;">
|
|
||||||
<button class="chatbutton bg-color-2" aria-label="Chatbox"><i class="fa-solid fa-comments fa-lg"></i> (<span class="chat-messages-count">0</span>)</button>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="flags-container-phone" class="panel-33">
|
<div id="flags-container-phone" class="panel-33">
|
||||||
@@ -316,12 +401,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="settings" aria-label="Settings"><i class="fa-solid fa-gear"></i></button>
|
|
||||||
<% if (chatEnabled) { %>
|
|
||||||
<button class="chatbutton hide-desktop bg-color-2" aria-label="Chatbox"><i class="fa-solid fa-comments fa-lg"></i> (<span class="chat-messages-count">0</span>)</button>
|
|
||||||
<% } %>
|
|
||||||
<button id="users-online-container" class="hide-phone" aria-label="Online users"><i class="fa-solid fa-user"></i> <span class="users-online"></span></button>
|
|
||||||
|
|
||||||
<div id="myModal" class="modal">
|
<div id="myModal" class="modal">
|
||||||
<div class="modal-panel">
|
<div class="modal-panel">
|
||||||
<div class="flex-container flex-phone" style="height: calc(100% - 100px)">
|
<div class="flex-container flex-phone" style="height: calc(100% - 100px)">
|
||||||
@@ -385,6 +464,9 @@
|
|||||||
<% } else if (isTuneAuthenticated) { %>
|
<% } else if (isTuneAuthenticated) { %>
|
||||||
<p class="color-3">You are logged in and can control the receiver.</p>
|
<p class="color-3">You are logged in and can control the receiver.</p>
|
||||||
<div class="admin-quick-dashboard">
|
<div class="admin-quick-dashboard">
|
||||||
|
<div class="icon tooltip <% if (!publicTuner) { %>active<% } %>" id="dashboard-lock-tune" onClick="toggleLock('#dashboard-lock-tune', 'wT1', 'wT0', 'Unlock Tuner (Password tune)', 'Lock Tuner (Password tune)');" role="button" aria-label="Toggle password lock until disconnect" tabindex="0" data-tooltip="Toggle password lock<br>Lasts until disconnect">
|
||||||
|
<i class="fa-solid fa-key"></i>
|
||||||
|
</div>
|
||||||
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
|
<div class="icon tooltip logout-link" role="button" aria-label="Sign out" tabindex="0" data-tooltip="Sign out">
|
||||||
<i class="fa-solid fa-sign-out"></i>
|
<i class="fa-solid fa-sign-out"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,36 +481,6 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
<div id="login-message" class="color-3"> </div>
|
<div id="login-message" class="color-3"> </div>
|
||||||
|
|
||||||
<hr class="color-2 auto">
|
|
||||||
|
|
||||||
<table class="auto text-left p-10">
|
|
||||||
<tr>
|
|
||||||
<td>Device:</td>
|
|
||||||
<td class="color-3 p-left-10">
|
|
||||||
<% if (device == 'tef') { %>TEF668x<% } %>
|
|
||||||
<% if (device == 'xdr') { %>Sony XDR<% } %>
|
|
||||||
<% if (device == 'sdr') { %>SDR<% } %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Server Time:</td>
|
|
||||||
<td class="color-3 p-left-10" id="server-time"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Local Time:</td>
|
|
||||||
<td class="color-3 p-left-10" id="client-time"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="auto">
|
|
||||||
<tr>
|
|
||||||
<td class="p-left-10">P1: <a class="color-3 pointer" id="preset1">87.5</a></td>
|
|
||||||
<td class="p-left-10">P2: <a class="color-3 pointer" id="preset2"></a></td>
|
|
||||||
<td class="p-left-10">P3: <a class="color-3 pointer" id="preset3"></a></td>
|
|
||||||
<td class="p-left-10">P4: <a class="color-3 pointer" id="preset4"></a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span>
|
FM-DX Webserver <span style="color: var(--color-3);" class="version-string"></span>
|
||||||
@@ -436,11 +488,6 @@
|
|||||||
<p class="text-small m-0 color-3">by <a href="https://fmdx.org" target="_blank">FMDX.org</a></p>
|
<p class="text-small m-0 color-3">by <a href="https://fmdx.org" target="_blank">FMDX.org</a></p>
|
||||||
<span class="text-small" style="color: var(--color-3);">[<a href="https://servers.fmdx.org/" target="_blank">Receiver Map</a>]</span>
|
<span class="text-small" style="color: var(--color-3);">[<a href="https://servers.fmdx.org/" target="_blank">Receiver Map</a>]</span>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
|
||||||
<% if(ownerContact){ %>
|
|
||||||
<span>Owner contact:</span><br>
|
|
||||||
<span class="text-small m-0 bottom-20"><%= ownerContact %></span>
|
|
||||||
<% } %>
|
|
||||||
<p class="text-small color-3" id="current-ping"></p>
|
<p class="text-small color-3" id="current-ping"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,8 +516,10 @@
|
|||||||
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close chat" tabindex="0"><i class="fa-solid fa-chevron-down"></i></div>
|
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close chat" tabindex="0"><i class="fa-solid fa-chevron-down"></i></div>
|
||||||
<div class="modal-panel-content text-left">
|
<div class="modal-panel-content text-left">
|
||||||
<div style="text-align: center;white-space-collapse: collapse;">
|
<div style="text-align: center;white-space-collapse: collapse;">
|
||||||
|
<div class="flex-phone flex-container flex-center top-10">
|
||||||
<input type="text" id="chat-nickname" name="chat-nickname" placeholder="Nickname" style="border-radius: 15px 0 0 15px;padding-top:0;padding-bottom:0;border: 2px solid var(--color-4)">
|
<input type="text" id="chat-nickname" name="chat-nickname" placeholder="Nickname" style="border-radius: 15px 0 0 15px;padding-top:0;padding-bottom:0;border: 2px solid var(--color-4)">
|
||||||
<button class="br-0 w-100 top-10" style="height: 48px; border-radius: 0 15px 15px 0;margin-left:-3px;" id="chat-nickname-save">Save</button>
|
<button class="br-0 w-100" style="height: 48px; border-radius: 0 15px 15px 0;margin-left:-3px;" id="chat-nickname-save">Save</button>
|
||||||
|
</div>
|
||||||
<p style="margin: 5px;" class="text-small">
|
<p style="margin: 5px;" class="text-small">
|
||||||
Current identity: <span style="color: #bada55;" id="chat-admin"></span> <strong id="chat-identity-nickname"></strong>
|
Current identity: <span style="color: #bada55;" id="chat-admin"></span> <strong id="chat-identity-nickname"></strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -497,3 +546,4 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ $(document).ready(function() {
|
|||||||
chatMessageCount++;
|
chatMessageCount++;
|
||||||
chatMessagesCount.text(chatMessageCount);
|
chatMessagesCount.text(chatMessageCount);
|
||||||
chatMessagesCount.attr("aria-label", "Chat (" + chatMessageCount + " unread)");
|
chatMessagesCount.attr("aria-label", "Chat (" + chatMessageCount + " unread)");
|
||||||
chatButton.removeClass('bg-color-2').addClass('blink');
|
chatButton.removeClass('bg-color-1').addClass('blink');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ $(document).ready(function() {
|
|||||||
chatButton.click(function() {
|
chatButton.click(function() {
|
||||||
chatMessageCount = 0;
|
chatMessageCount = 0;
|
||||||
chatMessagesCount.text(chatMessageCount);
|
chatMessagesCount.text(chatMessageCount);
|
||||||
chatButton.removeClass('blink').addClass('bg-color-2');
|
chatButton.removeClass('blink').addClass('bg-color-1');
|
||||||
chatSendInput.focus();
|
chatSendInput.focus();
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
var currentDate = new Date('Feb 9, 2025 18:00:00');
|
var currentDate = new Date('Feb 16, 2025 15:00:00');
|
||||||
var day = currentDate.getDate();
|
var day = currentDate.getDate();
|
||||||
var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1
|
var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1
|
||||||
var year = currentDate.getFullYear();
|
var year = currentDate.getFullYear();
|
||||||
var formattedDate = day + '/' + month + '/' + year;
|
var formattedDate = day + '/' + month + '/' + year;
|
||||||
var currentVersion = 'v1.3.4 [' + formattedDate + ']';
|
var currentVersion = 'v1.3.5 [' + formattedDate + ']';
|
||||||
|
|
||||||
getInitialSettings();
|
getInitialSettings();
|
||||||
removeUrlParameters();
|
removeUrlParameters();
|
||||||
|
|||||||
134
web/js/main.js
134
web/js/main.js
@@ -171,6 +171,10 @@ $(document).ready(function () {
|
|||||||
setInterval(sendPingRequest, 5000);
|
setInterval(sendPingRequest, 5000);
|
||||||
sendPingRequest();
|
sendPingRequest();
|
||||||
|
|
||||||
|
$("#tuner-name").click(function() {
|
||||||
|
showTunerDescription();
|
||||||
|
});
|
||||||
|
|
||||||
var freqUpButton = $('#freq-up')[0];
|
var freqUpButton = $('#freq-up')[0];
|
||||||
var freqDownButton = $('#freq-down')[0];
|
var freqDownButton = $('#freq-down')[0];
|
||||||
var psContainer = $('#ps-container')[0];
|
var psContainer = $('#ps-container')[0];
|
||||||
@@ -684,6 +688,31 @@ function checkKey(e) {
|
|||||||
case 39:
|
case 39:
|
||||||
tuneUp();
|
tuneUp();
|
||||||
break;
|
break;
|
||||||
|
case 46:
|
||||||
|
let $dropdown = $("#data-ant");
|
||||||
|
let $input = $dropdown.find("input");
|
||||||
|
let $options = $dropdown.find("ul.options .option");
|
||||||
|
|
||||||
|
if ($options.length === 0) return; // No antennas available
|
||||||
|
|
||||||
|
// Find the currently selected antenna
|
||||||
|
let currentText = $input.attr("placeholder").trim();
|
||||||
|
let currentIndex = $options.index($options.filter(function () {
|
||||||
|
return $(this).text().trim() === currentText;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Cycle to the next option
|
||||||
|
let nextIndex = (currentIndex + 1) % $options.length;
|
||||||
|
let $nextOption = $options.eq(nextIndex);
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
$input.attr("placeholder", $nextOption.text());
|
||||||
|
$input.data("value", $nextOption.data("value"));
|
||||||
|
|
||||||
|
// Send socket message (e.g., "Z0", "Z1", ...)
|
||||||
|
let socketMessage = "Z" + $nextOption.data("value");
|
||||||
|
socket.send(socketMessage);
|
||||||
|
break;
|
||||||
case 112: // F1
|
case 112: // F1
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
tuneTo(Number(localStorage.getItem('preset1')));
|
tuneTo(Number(localStorage.getItem('preset1')));
|
||||||
@@ -895,11 +924,10 @@ const updateDataElements = throttle(function(parsedData) {
|
|||||||
}
|
}
|
||||||
updateHtmlIfChanged($dataPs, parsedData.ps === '?' ? "<span class='opacity-half'>?</span>" : processString(parsedData.ps, parsedData.ps_errors));
|
updateHtmlIfChanged($dataPs, parsedData.ps === '?' ? "<span class='opacity-half'>?</span>" : processString(parsedData.ps, parsedData.ps_errors));
|
||||||
|
|
||||||
let stereoColor;
|
|
||||||
if(parsedData.st) {
|
if(parsedData.st) {
|
||||||
stereoColor = 'var(--color-4)';
|
$dataSt.parent().removeClass('opacity-half');
|
||||||
} else {
|
} else {
|
||||||
stereoColor = 'var(--color-3)';
|
$dataSt.parent().addClass('opacity-half');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(parsedData.stForced) {
|
if(parsedData.stForced) {
|
||||||
@@ -914,7 +942,6 @@ const updateDataElements = throttle(function(parsedData) {
|
|||||||
$('.data-st.circle1').css('left', '0px');
|
$('.data-st.circle1').css('left', '0px');
|
||||||
$('.data-st.circle2').css('display', 'block');
|
$('.data-st.circle2').css('display', 'block');
|
||||||
}
|
}
|
||||||
$dataSt.css('border', '2px solid ' + stereoColor);
|
|
||||||
|
|
||||||
updateHtmlIfChanged($dataRt0, processString(parsedData.rt0, parsedData.rt0_errors));
|
updateHtmlIfChanged($dataRt0, processString(parsedData.rt0, parsedData.rt0_errors));
|
||||||
updateHtmlIfChanged($dataRt1, processString(parsedData.rt1, parsedData.rt1_errors));
|
updateHtmlIfChanged($dataRt1, processString(parsedData.rt1, parsedData.rt1_errors));
|
||||||
@@ -1075,64 +1102,109 @@ function toggleLock(buttonSelector, activeMessage, inactiveMessage, activeLabel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showTunerDescription() {
|
||||||
|
let parentDiv = $("#dashboard-panel-description").parent();
|
||||||
|
|
||||||
function initTooltips() {
|
if (!$("#dashboard-panel-description").is(":visible")) {
|
||||||
$('.tooltip').hover(function (e) {
|
parentDiv.css("border-radius", "15px 15px 0 0");
|
||||||
// Check if hovered element is NOT `.popup-content`
|
}
|
||||||
if ($(e.target).closest('.popup-content').length) {
|
|
||||||
|
$("#dashboard-panel-description").slideToggle(300, function() {
|
||||||
|
if (!$(this).is(":visible")) {
|
||||||
|
parentDiv.css("border-radius", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($(window).width() < 768) {
|
||||||
|
$('.dashboard-panel-plugin-list').slideToggle(300);
|
||||||
|
$('.chatbutton').slideToggle(300);
|
||||||
|
$('#settings').slideToggle(300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTooltips(target = null) {
|
||||||
|
// Define scope: all tooltips or specific one if target is provided
|
||||||
|
const tooltips = target ? $(target) : $('.tooltip');
|
||||||
|
|
||||||
|
// Unbind existing event handlers before rebinding to avoid duplication
|
||||||
|
tooltips.off('mouseenter mouseleave');
|
||||||
|
|
||||||
|
tooltips.hover(function () {
|
||||||
|
if ($(this).closest('.popup-content').length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tooltipText = $(this).data('tooltip');
|
var tooltipText = $(this).data('tooltip');
|
||||||
|
var placement = $(this).data('tooltip-placement') || 'top'; // Default to 'top'
|
||||||
|
|
||||||
// Delay tooltip appearance
|
// Clear existing timeouts
|
||||||
$(this).data('timeout', setTimeout(() => {
|
$(this).data('timeout', setTimeout(() => {
|
||||||
if ($('.tooltip-wrapper').length === 0) {
|
$('.tooltip-wrapper').remove();
|
||||||
|
|
||||||
var tooltip = $(`
|
var tooltip = $(`
|
||||||
<div class="tooltip-wrapper">
|
<div class="tooltip-wrapper">
|
||||||
<div class="tooltiptext">
|
<div class="tooltiptext">${tooltipText}</div>
|
||||||
${tooltipText}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
$('body').append(tooltip);
|
$('body').append(tooltip);
|
||||||
}
|
|
||||||
|
|
||||||
var posX = e.pageX;
|
|
||||||
var posY = e.pageY;
|
|
||||||
var tooltipEl = $('.tooltiptext');
|
var tooltipEl = $('.tooltiptext');
|
||||||
var tooltipWidth = tooltipEl.outerWidth();
|
var tooltipWidth = tooltipEl.outerWidth();
|
||||||
var tooltipHeight = tooltipEl.outerHeight();
|
var tooltipHeight = tooltipEl.outerHeight();
|
||||||
posX -= tooltipWidth / 2;
|
var targetEl = $(this);
|
||||||
posY -= tooltipHeight + 10;
|
var targetOffset = targetEl.offset();
|
||||||
|
var targetWidth = targetEl.outerWidth();
|
||||||
|
var targetHeight = targetEl.outerHeight();
|
||||||
|
|
||||||
|
// Compute position
|
||||||
|
var posX, posY;
|
||||||
|
switch (placement) {
|
||||||
|
case 'bottom':
|
||||||
|
posX = targetOffset.left + targetWidth / 2 - tooltipWidth / 2;
|
||||||
|
posY = targetOffset.top + targetHeight + 10;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
posX = targetOffset.left - tooltipWidth - 10;
|
||||||
|
posY = targetOffset.top + targetHeight / 2 - tooltipHeight / 2;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
posX = targetOffset.left + targetWidth + 10;
|
||||||
|
posY = targetOffset.top + targetHeight / 2 - tooltipHeight / 2;
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
default:
|
||||||
|
posX = targetOffset.left + targetWidth / 2 - tooltipWidth / 2;
|
||||||
|
posY = targetOffset.top - tooltipHeight - 10;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply positioning
|
||||||
tooltipEl.css({ top: posY, left: posX, opacity: 1 });
|
tooltipEl.css({ top: posY, left: posX, opacity: 1 });
|
||||||
|
|
||||||
}, 500));
|
}, 300));
|
||||||
}, function () {
|
}, function () {
|
||||||
clearTimeout($(this).data('timeout'));
|
clearTimeout($(this).data('timeout'));
|
||||||
setTimeout(() => { $('.tooltip-wrapper').remove(); }, 500);
|
|
||||||
}).mousemove(function (e) {
|
|
||||||
var tooltipEl = $('.tooltiptext');
|
|
||||||
var tooltipWidth = tooltipEl.outerWidth();
|
|
||||||
var tooltipHeight = tooltipEl.outerHeight();
|
|
||||||
var posX = e.pageX - tooltipWidth / 2;
|
|
||||||
var posY = e.pageY - tooltipHeight - 10;
|
|
||||||
|
|
||||||
tooltipEl.css({ top: posY, left: posX });
|
setTimeout(() => {
|
||||||
|
$('.tooltip-wrapper').fadeOut(300, function () {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent the tooltip from showing when hovering over .popup-content
|
$('.popup-content').off('mouseenter').on('mouseenter', function () {
|
||||||
$('.popup-content').on('mouseenter', function (e) {
|
|
||||||
clearTimeout($('.tooltip').data('timeout'));
|
clearTimeout($('.tooltip').data('timeout'));
|
||||||
$('.tooltip-wrapper').remove(); // Ensure tooltip does not appear
|
$('.tooltip-wrapper').fadeOut(300, function () {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function fillPresets() {
|
function fillPresets() {
|
||||||
for (let i = 1; i <= 4; i++) {
|
for (let i = 1; i <= 4; i++) {
|
||||||
let presetText = localStorage.getItem(`preset${i}`);
|
let presetText = localStorage.getItem(`preset${i}`);
|
||||||
$(`#preset${i}`).text(presetText);
|
$(`#preset${i}-text`).text(presetText);
|
||||||
$(`#preset${i}`).click(function() {
|
$(`#preset${i}`).click(function() {
|
||||||
tuneTo(Number(presetText));
|
tuneTo(Number(presetText));
|
||||||
});
|
});
|
||||||
|
|||||||
61
web/js/plugins.js
Normal file
61
web/js/plugins.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
function checkScroll() {
|
||||||
|
let $container = $(".scrollable-container");
|
||||||
|
let $leftArrow = $(".scroll-left");
|
||||||
|
let $rightArrow = $(".scroll-right");
|
||||||
|
|
||||||
|
let scrollWidth = $container[0].scrollWidth;
|
||||||
|
let clientWidth = $container[0].clientWidth;
|
||||||
|
let scrollLeft = $container.scrollLeft();
|
||||||
|
let maxScrollLeft = scrollWidth - clientWidth;
|
||||||
|
|
||||||
|
if (scrollWidth > clientWidth) {
|
||||||
|
// If scrolling is possible, show arrows
|
||||||
|
$leftArrow.stop(true, true).fadeIn(200).css("pointer-events", scrollLeft > 0 ? "auto" : "none").fadeTo(200, scrollLeft > 0 ? 1 : 0.2);
|
||||||
|
$rightArrow.stop(true, true).fadeIn(200).css("pointer-events", scrollLeft < maxScrollLeft ? "auto" : "none").fadeTo(200, scrollLeft < maxScrollLeft ? 1 : 0.2);
|
||||||
|
} else {
|
||||||
|
// No scrolling needed, fully hide arrows
|
||||||
|
$leftArrow.stop(true, true).fadeOut(200);
|
||||||
|
$rightArrow.stop(true, true).fadeOut(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
let $container = $(".scrollable-container");
|
||||||
|
let $leftArrow = $(".scroll-left");
|
||||||
|
let $rightArrow = $(".scroll-right");
|
||||||
|
|
||||||
|
// Scroll left/right when arrows are clicked
|
||||||
|
$leftArrow.on("click", function () {
|
||||||
|
$container.animate({ scrollLeft: "-=100" }, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
$rightArrow.on("click", function () {
|
||||||
|
$container.animate({ scrollLeft: "+=100" }, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect scrolling
|
||||||
|
$container.on("scroll", checkScroll);
|
||||||
|
|
||||||
|
// Run checkScroll on page load to adjust visibility
|
||||||
|
setTimeout(checkScroll, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to add buttons dynamically
|
||||||
|
function addIconToPluginPanel(id, text, iconType, icon, tooltip) {
|
||||||
|
let $pluginButton = $(`
|
||||||
|
<button class="no-bg color-4 hover-brighten ${tooltip ? "tooltip" : ""}"
|
||||||
|
style="padding: 6px; width: 64px; min-width: 64px;" id="${id}"
|
||||||
|
data-tooltip="${tooltip ? tooltip : ""}" data-tooltip-placement="bottom">
|
||||||
|
<i class="fa-${iconType} fa-${icon} fa-lg top-10"></i><br>
|
||||||
|
<span style="font-size: 10px; color: var(--color-main-bright) !important;">${text}</span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
$('.scrollable-container').append($pluginButton);
|
||||||
|
initTooltips($pluginButton);
|
||||||
|
|
||||||
|
// Recheck scrolling when new buttons are added
|
||||||
|
setTimeout(checkScroll, 100);
|
||||||
|
}
|
||||||
@@ -209,7 +209,7 @@ function setTheme(themeName) {
|
|||||||
$(':root').css('--color-main-bright', themeColors[1]);
|
$(':root').css('--color-main-bright', themeColors[1]);
|
||||||
$(':root').css('--color-text', themeColors[2]);
|
$(':root').css('--color-text', themeColors[2]);
|
||||||
$(':root').css('--color-text-2', textColor2);
|
$(':root').css('--color-text-2', textColor2);
|
||||||
$('#wrapper-outer').css('background-color', backgroundColorWithOpacity);
|
$('.wrapper-outer').css('background-color', backgroundColorWithOpacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ $.getScript('./js/modal.js');
|
|||||||
$.getScript('./js/settings.js');
|
$.getScript('./js/settings.js');
|
||||||
$.getScript('./js/chat.js');
|
$.getScript('./js/chat.js');
|
||||||
$.getScript('./js/toast.js');
|
$.getScript('./js/toast.js');
|
||||||
|
$.getScript('./js/plugins.js');
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
<div id="wrapper-outer">
|
<div class="wrapper-outer wrapper-full">
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<div class="panel-100 no-bg">
|
<div class="panel-100 no-bg">
|
||||||
<img class="top-25" src="favicon.png" height="64px">
|
<img class="top-25" src="favicon.png" height="64px">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
<div id="wrapper-outer" class="wrapper-outer-static">
|
<div class="wrapper-outer wrapper-full wrapper-outer-static">
|
||||||
<div id="navigation" class="sidenav flex-container flex-phone">
|
<div id="navigation" class="sidenav flex-container flex-phone">
|
||||||
<div class="sidenav-content">
|
<div class="sidenav-content">
|
||||||
<h1 class="top-25">Settings</h1>
|
<h1 class="top-25">Settings</h1>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="toast-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
|
<div id="toast-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
|
||||||
<div id="wrapper-outer">
|
<div class="wrapper-outer wrapper-full">
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<div class="panel-100 no-bg">
|
<div class="panel-100 no-bg">
|
||||||
<img class="top-10" src="../images/openradio_logo_neutral.png" height="64px">
|
<img class="top-10" src="../images/openradio_logo_neutral.png" height="64px">
|
||||||
|
|||||||
Reference in New Issue
Block a user