1(() => { 2/////////////////////////////////////////////////////////////// 3/// /// 4/// SCANNER CLIENT SCRIPT FOR FM-DX-WEBSERVER (V4.3) /// 5/// /// 6/// by Highpoint last update: 20.04.2026 /// 7/// powered by PE5PVB /// 8/// /// 9/// https://github.com/Highpoint2000/webserver-scanner /// 10/// /// 11/////////////////////////////////////////////////////////////// 12 13 const pluginSetupOnlyNotify = true; 14 const CHECK_FOR_UPDATES = true; 15 16/////////////////////////////////////////////////////////////// 17 18 const pluginVersion = '4.3'; 19 const pluginName = "Scanner"; 20 const pluginHomepageUrl = "https://github.com/Highpoint2000/webserver-scanner/releases"; 21 const pluginUpdateUrl = "https://raw.githubusercontent.com/Highpoint2000/webserver-scanner/refs/heads/main/plugins/Scanner/scanner.js"; 22 23 let AvailableScannerConfigs = ["default"]; 24 let ActiveScannerConfig = 'default'; 25 const EnableBlacklist = true; // auto from config 26 const EnableWhitelist = true; // auto from config 27 const EnableSpectrumScan = true; // auto from config 28 const EnableSpectrumScanBL = true; // auto from config 29 const EnableDifferenceScan = true; // auto from config 30 const EnableDifferenceScanBL = true; // auto from config 31 32 let SignalStrengthUnit = 'dBµV'; // initial, will be updated from UI 33 34 const currentURL = new URL(window.location.href); 35 const WebserverURL = currentURL.hostname; 36 const WebserverPath = currentURL.pathname.replace(/setup/g, ''); 37 const WebserverPORT = currentURL.port || (currentURL.protocol === 'https:' ? '443' : '80'); 38 const protocol = currentURL.protocol === 'https:' ? 'wss:' : 'ws:'; 39 const WEBSOCKET_URL = `${protocol}//${WebserverURL}:${WebserverPORT}${WebserverPath}data_plugins`; 40 const target = '127.0.0.1'; 41 42 let wsSendSocket; 43 let clientIp = ''; 44 let isTuneAuthenticated = false; 45 let isTunerLocked = false; 46 let isTuningAllowed = true; 47 let scannerButtonsExecuted = false; 48 let Scan = 'off'; 49 let ScanPE5PVBstatus = ''; 50 let SpectrumLimiterValueStatus; 51 let ScannerModeStatus; 52 53 // Remember last internal values from server 54 let lastInternalSensitivity = null; 55 let lastScannerModeValue = null; 56 let lastScanHoldTimeValue = null; 57 58 /////////////////////////////////////////////////////////// 59 // Helper: map UI label to internal SignalStrengthUnit 60 /////////////////////////////////////////////////////////// 61 function mapUnitLabelToInternal(label) { 62 const txt = (label || '').toLowerCase(); 63 if (txt.includes('dbf')) return 'dBf'; 64 if (txt.includes('dbuv')) return 'dBµV'; // canonical 65 if (txt.includes('dbm')) return 'dBm'; 66 return SignalStrengthUnit; // fallback: keep previous 67 } 68 69 /////////////////////////////////////////////////////////// 70 // Create a status message object 71 /////////////////////////////////////////////////////////// 72 function createMessage(status, Scan = '', Search = '', Sensitivity = '', ScannerMode = '', ScanHoldTime = '') { 73 return { 74 type: 'Scanner', 75 value: { 76 status, 77 Scan, 78 Search, 79 Sensitivity, 80 ScannerMode, 81 ScanHoldTime, 82 }, 83 source: clientIp, 84 target: target 85 }; 86 } 87 88 /////////////////////////////////////////////////////////// 89 // Update-check for /setup 90 /////////////////////////////////////////////////////////// 91 function checkUpdate(setupOnly, pluginName, urlUpdateLink, urlFetchLink) { 92 if (setupOnly && window.location.pathname !== '/setup') return; 93 94 let pluginVersionCheck = 95 typeof pluginVersion !== 'undefined' ? pluginVersion : 96 typeof plugin_version !== 'undefined' ? plugin_version : 97 typeof PLUGIN_VERSION !== 'undefined' ? PLUGIN_VERSION : 98 'Unknown'; 99 100 async function fetchFirstLine() { 101 const urlCheckForUpdate = urlFetchLink; 102 103 try { 104 const response = await fetch(urlCheckForUpdate); 105 if (!response.ok) { 106 throw new Error(`[${pluginName}] update check HTTP error! status: ${response.status}`); 107 } 108 109 const text = await response.text(); 110 const lines = text.split('\n'); 111 112 let version; 113
114 // Robuster: RegEx mit \s* statt exaktem " =" 115 const versionRegex = /const\s+(?:pluginVersion|plugin_version|PLUGIN_VERSION)\s*=\s*['"]([^'"]+)['"]/; 116 117 const versionLine = lines.find(line => versionRegex.test(line)); 118 if (versionLine) { 119 const match = versionLine.match(versionRegex); 120 if (match) { 121 version = match[1]; 122 } 123 } 124 125 if (!version) { 126 const firstLine = lines[0].trim(); 127 version = /^\d/.test(firstLine) ? firstLine : "Unknown"; 128 } 129 130 return version; 131 } catch (error) { 132 console.error(`[${pluginName}] error fetching file:`, error); 133 return null; 134 } 135 } 136 137 138 fetchFirstLine().then(newVersion => { 139 if (newVersion && newVersion !== pluginVersionCheck) { 140 const updateConsoleText = "There is a new version of this plugin available"; 141 console.log(`[${pluginName}] ${updateConsoleText}`); 142 setupNotify(pluginVersionCheck, newVersion, pluginName, urlUpdateLink); 143 } 144 }); 145 146 function setupNotify(pluginVersionCheck, newVersion, pluginName, urlUpdateLink) { 147 if (window.location.pathname === '/setup') { 148 const pluginSettings = document.getElementById('plugin-settings'); 149 if (pluginSettings) { 150 const currentText = pluginSettings.textContent.trim(); 151 const newText = `<a href="${urlUpdateLink}" target="_blank">[${pluginName}] Update available: ${pluginVersionCheck} --> ${newVersion}</a><br>`; 152 153 if (currentText === 'No plugin settings are available.') { 154 pluginSettings.innerHTML = newText; 155 } else { 156 pluginSettings.innerHTML += ' ' + newText; 157 } 158 } 159 160 const updateIcon = 161 document.querySelector('.wrapper-outer #navigation .sidenav-content .fa-puzzle-piece') || 162 document.querySelector('.wrapper-outer .sidenav-content') || 163 document.querySelector('.sidenav-content'); 164 165 const redDot = document.createElement('span'); 166 redDot.style.display = 'block'; 167 redDot.style.width = '12px'; 168 redDot.style.height = '12px'; 169 redDot.style.borderRadius = '50%'; 170 redDot.style.backgroundColor = '#FE0830' || 'var(--color-main-bright)'; 171 redDot.style.marginLeft = '82px'; 172 redDot.style.marginTop = '-12px'; 173 174 if (updateIcon) { 175 updateIcon.appendChild(redDot); 176 } 177 } 178 } 179 } 180 181 if (CHECK_FOR_UPDATES) { 182 checkUpdate(pluginSetupOnlyNotify, pluginName, pluginHomepageUrl, pluginUpdateUrl); 183 } 184 185 /////////////////////////////////////////////////////////// 186 // Initial WebSocket request 187 /////////////////////////////////////////////////////////// 188 async function sendInitialWebSocketMessage() { 189 try { 190 const response = await fetch('https://icanhazip.com'); 191 clientIp = (await response.text()).trim(); 192 console.log("Public IP address:", clientIp); 193 194 const initialMessage = createMessage('request'); 195 196 if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) { 197 wsSendSocket.send(JSON.stringify(initialMessage)); 198 console.log("Scanner sent initial message:", initialMessage); 199 } else { 200 console.error("Scanner Error! WebSocket is not open. Cannot send initial message."); 201 sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send initial message.', false, false); 202 } 203 } catch (error) { 204 console.error("Error fetching public IP address:", error); 205 sendToast('error important', 'Scanner', 'Error fetching public IP address.', false, false); 206 } 207 } 208
209 /////////////////////////////////////////////////////////// 210 // Commands over WebSocket 211 /////////////////////////////////////////////////////////// 212 async function sendSearch(SearchFunction) { 213 try { 214 const searchMessage = createMessage('command', '', SearchFunction); 215 if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) { 216 wsSendSocket.send(JSON.stringify(searchMessage)); 217 console.log("Search message sent:", searchMessage); 218 } else { 219 console.error("Scanner Error! WebSocket is not open. Cannot send search message."); 220 sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false); 221 } 222 } catch (error) { 223 console.error(error); 224 } 225 } 226 227 async function sendScan(ScanFunction) { 228 try { 229 const scanMessage = createMessage('command', ScanFunction); 230 if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) { 231 wsSendSocket.send(JSON.stringify(scanMessage)); 232 console.log("Scanner sent message:", scanMessage); 233 } else { 234 console.error("Scanner Error! WebSocket is not open. Cannot send scan message."); 235 sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false); 236 } 237 } catch (error) { 238 console.error(error); 239 } 240 } 241 242 async function SendValue(Sensitivity, ScannerMode, ScanHoldTime) { 243 const ssu = (SignalStrengthUnit || '').toLowerCase(); 244 let resultSensitivity; 245 246 if (ssu === 'dbuv' || ssu === 'dbµv' || ssu === 'dbμv') { 247 resultSensitivity = Math.round(parseFloat(Sensitivity) + 10.875); 248 } else if (ssu === 'dbm') { 249 resultSensitivity = Math.round(parseFloat(Sensitivity) + 119.75); 250 } else if (ssu === 'dbf') { 251 resultSensitivity = Math.round(parseFloat(Sensitivity)); 252 } else { 253 resultSensitivity = Math.round(parseFloat(Sensitivity)); 254 } 255 256 try { 257 const valueMessage = createMessage('command', '', '', resultSensitivity, ScannerMode, ScanHoldTime); 258 if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) { 259 wsSendSocket.send(JSON.stringify(valueMessage)); 260 console.log("Value message sent:", valueMessage); 261 } else { 262 console.error("Scanner Error! WebSocket is not open. Cannot send value message."); 263 sendToast('error important', 'Scanner', 'WebSocket is not open. Cannot send value message.', false, false); 264 } 265 } catch (error) { 266 console.error(error); 267 } 268 } 269 270 /////////////////////////////////////////////////////////// 271 // WebSocket connection setup 272 /////////////////////////////////////////////////////////// 273 async function setupSendSocket() { 274 if (!wsSendSocket || wsSendSocket.readyState === WebSocket.CLOSED) { 275 try { 276 wsSendSocket = new WebSocket(WEBSOCKET_URL); 277 278 wsSendSocket.onopen = () => { 279 console.log("Scanner connected WebSocket"); 280 sendInitialWebSocketMessage(); 281 }; 282 283 wsSendSocket.onmessage = handleWebSocketMessage; 284 285 wsSendSocket.onerror = (error) => { 286 console.error("Scanner Websocket Error:", error); 287 sendToast('error important', 'Scanner', 'Websocket Error', false, false); 288 }; 289 290 wsSendSocket.onclose = (event) => { 291 console.log("Scanner Error! Websocket closed or not open:", event); 292 setTimeout(setupSendSocket, 5000); 293 }; 294 } catch (error) { 295 console.error("Failed to setup Send WebSocket:", error); 296 sendToast('error important', 'Scanner', 'Failed to setup Send WebSocket', false, false); 297 setTimeout(setupSendSocket, 5000); 298 } 299 } 300 } 301
302 /////////////////////////////////////////////////////////// 303 // Sensitivity normalization (internal â UI) 304 /////////////////////////////////////////////////////////// 305 function normalizeSensitivity(Sensitivity, unitStr) { 306 const ssu = (unitStr || '').toLowerCase(); 307 let result; 308 309 if (ssu === 'dbuv' || ssu === 'dbµv' || ssu === 'dbμv') { 310 result = parseFloat(Sensitivity) - 10.875; 311 } else if (ssu === 'dbm') { 312 result = parseFloat(Sensitivity) - 119.75; 313 } else if (ssu === 'dbf') { 314 result = parseFloat(Sensitivity); 315 } else { 316 result = parseFloat(Sensitivity); 317 } 318 319 return Math.round(result); 320 } 321 322 /////////////////////////////////////////////////////////// 323 // Toast with cooldown 324 /////////////////////////////////////////////////////////// 325 let lastToastTime = 0; 326 327 function sendToastWithCooldown(type, title, message, autoClose = true, closeOnClick = true) { 328 const currentTime = Date.now(); 329 if (currentTime - lastToastTime < 150) { 330 return; 331 } 332 lastToastTime = currentTime; 333 sendToast(type, title, message, autoClose, closeOnClick); 334 } 335 336 /////////////////////////////////////////////////////////// 337 // WebSocket message handler 338 /////////////////////////////////////////////////////////// 339 function handleWebSocketMessage(event) { 340 try { 341 const eventData = JSON.parse(event.data); 342 343 if (eventData.source !== clientIp) { 344 // console.log(eventData); 345 } 346 347 if (eventData.type === 'Scanner') { 348 const { 349 status, 350 ScanPE5PVB, 351 SearchPE5PVB, 352 Scan, 353 Sensitivity, 354 ScannerMode, 355 ScanHoldTime, 356 SpectrumLimiterValue, 357 StatusFMLIST, 358 InfoFMLIST, 359 AvailableConfigs, 360 ActiveConfig 361 } = eventData.value; 362 363 console.log(eventData.value); 364 365 if (typeof ScanPE5PVB !== 'undefined') { 366 ScanPE5PVBstatus = ScanPE5PVB; 367 } 368 369 // Update Dynamic Configuration lists if provided by WebSocket 370 let configsChanged = false; 371 if (AvailableConfigs !== undefined) { 372 if (JSON.stringify(AvailableConfigs) !== JSON.stringify(AvailableScannerConfigs)) { 373 AvailableScannerConfigs = AvailableConfigs; 374 configsChanged = true; 375 } 376 } 377 if (ActiveConfig !== undefined) { 378 if (ActiveConfig !== ActiveScannerConfig) { 379 ActiveScannerConfig = ActiveConfig; 380 configsChanged = true; 381 } 382 } 383 384 if (configsChanged && isTuneAuthenticated) { 385 updateAdminScannerDropdownUI(); 386 } 387 388 // IMPORTANT: Only store valid (non-empty) values for later re-render to prevent NaN issues 389 if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') { 390 lastInternalSensitivity = Sensitivity; 391 } 392 if (ScannerMode !== undefined && ScannerMode !== null && ScannerMode !== '') { 393 lastScannerModeValue = ScannerMode; 394 } 395 if (ScanHoldTime !== undefined && ScanHoldTime !== null && ScanHoldTime !== '') { 396 lastScanHoldTimeValue = ScanHoldTime; 397 } 398 399 if (status === 'response') { 400 if (!scannerButtonsExecuted) { 401 ScannerButtons(Sensitivity, ScannerMode, ScanHoldTime); 402 SearchButtons(); 403 scannerButtonsExecuted = true; 404 405 if (isTuneAuthenticated) { 406 if ( 407 ScannerMode === 'spectrum' || 408 ScannerMode === 'spectrumBL' || 409 ScannerMode === 'difference' || 410 ScannerMode === 'differenceBL' 411 ) { 412 sendToastWithCooldown( 413 'info', 414 'Scanner', 415 `Settings activated! PE5PVB Scan: ${ScanPE5PVB} | PE5PVB Search: ${SearchPE5PVB} | Autoscan: ${Scan} | Sensitivity: ${Sensitivity} | Limit: ${SpectrumLimiterValue} | Scanmode: ${ScannerMode} | Scanholdtime: ${ScanHoldTime}`, 416 false, 417 false 418 ); 419 if (SpectrumLimiterValue !== 'undefined' && SpectrumLimiterValue !== '') { 420 SpectrumLimiterValueStatus = SpectrumLimiterValue; 421 ScannerModeStatus = `${ScannerMode}`; 422 } 423 } else { 424 sendToastWithCooldown( 425 'info', 426 'Scanner', 427 `Settings activated! PE5PVB Scan: ${ScanPE5PVB} | PE5PVB Search: ${SearchPE5PVB} | Autoscan: ${Scan} | Sensitivity: ${Sensitivity} | Scanmode: ${ScannerMode} | Scanholdtime: ${ScanHoldTime}`, 428 false, 429 false 430 ); 431 } 432 } 433 } 434 435 // Protect normalizeSensitivity from processing undefined or empty strings 436 if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') { 437 const normSens = normalizeSensitivity(Sensitivity, SignalStrengthUnit); 438 updateDropdownValues(normSens, ScannerMode, ScanHoldTime); 439 } 440 441 } else if ( 442 status === 'broadcast' && 443 InfoFMLIST && 444 InfoFMLIST.includes("successful") 445 ) { 446 sendToastWithCooldown('success important', 'Scanner', `${InfoFMLIST}`, false, false); 447 sendInitialWebSocketMessage(); 448 449 } else if ( 450 status === 'broadcast' && 451 InfoFMLIST && 452 InfoFMLIST.includes("failed") 453 ) { 454 sendToastWithCooldown('error important', 'Scanner', `${InfoFMLIST}`, false, false); 455 sendInitialWebSocketMessage(); 456 457 } else if (status === 'broadcast' || status === 'send') { 458 if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '') { 459 const normSens = normalizeSensitivity(Sensitivity, SignalStrengthUnit); 460 updateDropdownValues(normSens, ScannerMode, ScanHoldTime); 461 } 462 } 463 464 // Scan button behaviour (UI only) 465 const ScanButton = document.getElementById('Scan-on-off'); 466 const blinkTextElement = document.querySelector('#tune-buttons .autoscan-blink'); 467 const scannerControls = document.getElementById('scanner-control
467s'); 468 const HideElement = document.querySelector('.panel-33.hide-phone.no-bg'); 469 const volumeSlider = document.getElementById('volumeSlider'); 470 const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null; 471 472 if (ScanButton) { 473 if (Scan === 'off') { 474 const element = document.getElementById('log-fmlist'); 475 if (element) element.style.display = 'block'; 476 477 ScanButton.setAttribute('data-scan-status', 'off'); 478 ScanButton.classList.add('bg-color-3'); 479 ScanButton.classList.remove('bg-color-4'); 480 481 const freqDownElement = document.getElementById('freq-down'); 482 if (freqDownElement) freqDownElement.style.display = 'block'; 483 484 const freqUpElement = document.getElementById('freq-up'); 485 if (freqUpElement) freqUpElement.style.display = 'block'; 486 487 const searchDownElement = document.getElementById('search-down'); 488 if (searchDownElement) searchDownElement.style.display = 'block'; 489 490 const searchUpElement = document.getElementById('search-up'); 491 if (searchUpElement) searchUpElement.style.display = 'block'; 492 493 const commandInputElement = document.getElementById('commandinput'); 494 if (commandInputElement) commandInputElement.style.display = 'block'; 495 496 if (blinkTextElement) blinkTextElement.style.display = 'none'; 497 498 if (window.innerWidth < 769) { 499 if (volumeSliderParent) volumeSliderParent.style.display = 'none'; 500 if (scannerControls) scannerControls.style.display = 'none'; 501 if (HideElement) HideElement.classList.add('hide-phone'); 502 } 503 504 } else if (Scan === 'on') { 505 const element = document.getElementById('log-fmlist'); 506 if (element) element.style.display = 'none'; 507 508 ScanButton.setAttribute('data-scan-status', 'on'); 509 ScanButton.classList.add('bg-color-4'); 510 ScanButton.classList.remove('bg-color-3'); 511 512 const freqDownElement = document.getElementById('freq-down'); 513 if (freqDownElement) freqDownElement.style.display = 'none'; 514 515 const freqUpElement = document.getElementById('freq-up'); 516 if (freqUpElement) freqUpElement.style.display = 'none'; 517 518 const searchDownElement = document.getElementById('search-down'); 519 if (searchDownElement) searchDownElement.style.display = 'none'; 520 521 const searchUpElement = document.getElementById('search-up'); 522 if (searchUpElement) searchUpElement.style.display = 'none'; 523 524 const commandInputElement = document.getElementById('commandinput'); 525 if (commandInputElement) commandInputElement.style.display = 'none'; 526 527 if (blinkTextElement) blinkTextElement.style.display = 'block'; 528 529 if (window.innerWidth < 769) { 530 if (volumeSliderParent) volumeSliderParent.style.display = 'none'; 531 if (scannerControls) scannerControls.style.display = 'flex'; 532 if (HideElement) HideElement.classList.remove('hide-phone'); 533 } 534 } 535 } 536 } 537 } catch (error) { 538 console.error("Error handling WebSocket message:", error); 539 } 540 } 541 542 /////////////////////////////////////////////////////////// 543 // Update dropdown values (display) 544 /////////////////////////////////////////////////////////// 545 function updateDropdownValues(Sensitivity, ScannerMode, ScanHoldTime) { 546 if (Sensitivity !== undefined && Sensitivity !== null && Sensitivity !== '' && !Number.isNaN(Sensitivity)) { 547 const sensitivityInput = document.querySelector('input[title="Scanner Sensitivity"]'); 548 if (sensitivityInput) { 549 if (ScanPE5PVBstatus) { 550 sensitivityInput.value = `${Sensitivity}`; 551 } else { 552 sensitivityInput.value = `${Sensitivity} ${SignalStrengthUnit}`; 553 } 554 sensitivityInput.setAttribute('data-value', Sensitivity); 555 } 556 } 557 558 if (ScannerMode !== undefined && ScannerMode !== null && ScannerMode !== '') { 559 const modeInput = document.querySelector('input[title="Scanner Mode"]'); 560 if (modeInput) { 561 modeInput.value = `${ScannerMode}`; 562 modeInput.setAttribute('data-value', ScannerMode); 563 } 564 } 565 566 if (ScanHoldTime !== undefined && ScanHoldTime !== null && ScanHoldTime !== '') { 567 const holdTimeInput = document.querySelector('input[title="Scanhold Time"]'); 568 if (holdTimeInput) { 569 holdTimeInput.value = `${ScanHoldTime} sec.`; 570 holdTimeInput.setAttribute('data-value', ScanHoldTime); 571 } 572 } 573 } 574
575 /////////////////////////////////////////////////////////// 576 // Search buttons 577 /////////////////////////////////////////////////////////// 578 function SearchButtons() { 579 const searchDownButton = document.createElement('button'); 580 searchDownButton.id = 'search-down'; 581 searchDownButton.setAttribute('aria-label', 'Scan Down'); 582 searchDownButton.classList.add('rectangular-downbutton'); 583 searchDownButton.innerHTML = '<i class="fa-solid fa-chevron-left"></i><i class="fa-solid fa-chevron-left"></i>'; 584 585 const searchUpButton = document.createElement('button'); 586 searchUpButton.id = 'search-up'; 587 searchUpButton.setAttribute('aria-label', 'Scan Up'); 588 searchUpButton.classList.add('rectangular-upbutton'); 589 searchUpButton.innerHTML = '<i class="fa-solid fa-chevron-right"></i><i class="fa-solid fa-chevron-right"></i>'; 590 591 const rectangularButtonStyle = ` 592 .rectangular-downbutton { 593 border: 3px solid #ccc; 594 border-radius: 0px; 595 padding: 5px 10px; 596 background-color: #fff; 597 color: #333; 598 cursor: pointer; 599 transition: background-color 0.3s, color 0.3s, border-color 0.3s; 600 margin-left: 1px; 601 } 602 603 .rectangular-upbutton { 604 border: 3px solid #ccc; 605 border-radius: 0px; 606 padding: 5px 10px; 607 background-color: #fff; 608 color: #333; 609 cursor: pointer; 610 transition: background-color 0.3s, color 0.3s, border-color 0.3s; 611 margin-right: 1px; 612 } 613 614 .rectangular-button:hover { 615 background-color: #f0f0f0; 616 border-color: #aaa; 617 } 618 `; 619 620 const styleElement = document.createElement('style'); 621 styleElement.innerHTML = rectangularButtonStyle; 622 document.head.appendChild(styleElement); 623 624 const freqDownButton = document.getElementById('freq-down'); 625 if (freqDownButton && freqDownButton.parentNode) { 626 freqDownButton.parentNode.insertBefore(searchDownButton, freqDownButton.nextSibling); 627 } 628 629 const freqUpButton = document.getElementById('freq-up'); 630 if (freqUpButton && freqUpButton.parentNode) { 631 freqUpButton.parentNode.insertBefore(searchUpButton, freqUpButton); 632 } 633 634 searchDownButton.addEventListener('click', function () { 635 if (!isTuningAllowed) { 636 sendToast('warning', 'Scanner', 'Tuner is currently locked to admin!', false, false); 637 return; 638 } 639 sendSearch('down'); 640 }); 641 642 searchUpButton.addEventListener('click', function () { 643 if (!isTuningAllowed) { 644 sendToast('warning', 'Scanner', 'Tuner is currently locked to admin!', false, false); 645 return; 646 } 647 sendSearch('up'); 648 }); 649 } 650 651 /////////////////////////////////////////////////////////// 652 // Autoscan blinking label 653 /////////////////////////////////////////////////////////// 654 function BlinkAutoScan() { 655 console.log('BlinkAutoScan started'); 656 657 const parentElement = document.getElementById('tune-buttons'); 658 if (parentElement) { 659 parentElement.classList.remove('no-bg'); 660 parentElement.style.position = 'relative'; 661 662 let blinkContainer = parentElement.querySelector('.autoscan-blink-container'); 663 if (!blinkContainer) { 664 blinkContainer = document.createElement('div'); 665 blinkContainer.className = 'autoscan-blink-container'; 666 blinkContainer.style.position = 'absolute'; 667 blinkContainer.style.top = '0'; 668 blinkContainer.style.left = '0'; 669 blinkContainer.style.width = '100%'; 670 blinkContainer.style.height = '100%'; 671 blinkContainer.style.pointerEvents = 'none'; 672 blinkContainer.style.display = 'none'; 673 parentElement.appendChild(blinkContainer); 674 } 675 676 const blinkText = document.createElement('span'); 677 blinkText.textContent = 'Autoscan active!'; 678 blinkText.classList.add('autoscan-blink'); 679 blinkText.style.display = 'none'; 680 blinkContainer.appendChild(blinkText); 681 682 const style = document.createElement('style'); 683 style.textContent = ` 684 .autoscan-blink { 685 font-size: 32px; 686 font-weight: bold; 687 font-family: "Titillium Web", Calibri, sans-serif; 688 color: red; 689 animation: autoscan-blink-animation 1s step-start infinite; 690 } 691 @keyframes autoscan-blink-animation { 692 50% { opacity: 0; } 693 } 694 `; 695 document.head.appendChild(style); 696 697 setTimeout(() => { 698 blinkContainer.style.display = 'block'; 699 }, 100); 700 } else { 701 console.warn('Element with the ID "tune-buttons" not found.'); 702 } 703 } 704
705 /////////////////////////////////////////////////////////// 706 // Cookie helpers 707 /////////////////////////////////////////////////////////// 708 function setCookie(name, value, days) { 709 const expires = new Date(Date.now() + days * 864e5).toUTCString(); 710 document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/'; 711 } 712 713 function getCookie(name) { 714 return document.cookie.split('; ').reduce((r, v) => { 715 const parts = v.split('='); 716 return parts[0] === name ? decodeURIComponent(parts[1]) : r; 717 }, ''); 718 } 719 720 function eraseCookie(name) { 721 setCookie(name, '', -1); 722 } 723 724 /////////////////////////////////////////////////////////// 725 // Scanner main button 726 /////////////////////////////////////////////////////////// 727 function ScannerButtons(Sensitivity, ScannerMode, ScanHoldTime) { 728 const ScannerButton = document.createElement('button'); 729 ScannerButton.id = 'Scan-on-off'; 730 ScannerButton.setAttribute('aria-label', 'Scan'); 731 ScannerButton.setAttribute('data-scan-status', 'off'); 732 ScannerButton.style.borderRadius = '0px'; 733 ScannerButton.style.position = 'relative'; 734 ScannerButton.style.top = '0px'; 735 736 if (Scan === 'off') { 737 ScannerButton.classList.add('bg-color-3'); 738 } 739 740 ScannerButton.title = `Plugin Version ${pluginVersion}`; 741 742 const spans = document.querySelectorAll('span.text-small.color-4'); 743 let hasSDR = false; 744 spans.forEach(span => { 745 if (span.textContent.trim() === 'SDR') { 746 hasSDR = true; 747 } 748 }); 749 750 if (hasSDR) { 751 ScannerButton.style.borderRadius = '15px'; 752 ScannerButton.style.width = '300%'; 753 ScannerButton.style.right = '100%'; 754 } else { 755 ScannerButton.style.marginLeft = '1px'; 756 ScannerButton.style.marginRight = '1px'; 757 758 if (window.innerWidth < 769) { 759 ScannerButton.style.width = '100%'; 760 ScannerButton.style.height = '48px'; 761 ScannerButton.innerHTML = '<strong>Autoscan</strong>'; 762 ScannerButton.style.borderRadius = '15px'; 763 } else if (window.innerWidth < 890) { 764 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 765 ScannerButton.style.borderRadius = '0px'; 766 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.0%' : '97.5%'; 767 } else if (window.innerWidth < 990) { 768 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 769 ScannerButton.style.borderRadius = '0px'; 770 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.4%' : '97.7%'; 771 } else if (window.innerWidth < 1180) { 772 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 773 ScannerButton.style.borderRadius = '0px'; 774 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '98.5%'; 775 } else { 776 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 777 ScannerButton.style.borderRadius = '0px'; 778 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '98.5%'; 779 } 780 781 const el = document.getElementById('data-ant'); 782 if (el) { 783 if (window.innerWidth < 769) { 784 ScannerButton.style.width = '100%'; 785 ScannerButton.style.height = '48px'; 786 ScannerButton.innerHTML = '<strong>Autoscan</strong>'; 787 ScannerButton.style.borderRadius = '15px'; 788 } else if (window.innerWidth < 890) { 789 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 790 ScannerButton.style.borderRadius = '0px'; 791 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '96.5%' : '95.0%'; 792 } else if (window.innerWidth < 990) { 793 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 794 ScannerButton.style.borderRadius = '0px'; 795 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.4%' : '97.0%'; 796 } else if (window.innerWidth < 1180) { 797 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 798 ScannerButton.style.borderRadius = '0px'; 799 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '97.
7990%' : '97.4%'; 800 } else { 801 ScannerButton.innerHTML = '<strong>Auto<br>Scan</strong>'; 802 ScannerButton.style.borderRadius = '0px'; 803 ScannerButton.style.width = (window.innerWidth % 2 !== 0) ? '98.0%' : '97.5%'; 804 } 805 } 806 } 807 808 let pressTimer; 809 let isLongPress = false; 810 811 ScannerButton.addEventListener('click', function () { 812 const isActive = ScannerButton.getAttribute('data-scan-status') === 'on'; 813 if (!isLongPress) { 814 if (isActive) { 815 ScannerButton.setAttribute('data-scan-status', 'off'); 816 ScannerButton.style.backgroundColor = 'var(--color-3)'; 817 } else { 818 ScannerButton.setAttribute('data-scan-status', 'on'); 819 ScannerButton.style.backgroundColor = 'var(--color-4)'; 820 } 821 } 822 }); 823 824 if (window.innerWidth < 769 && ScannerButton) { 825 const popupContent = document.querySelector('.popup-content'); 826 if (popupContent) { 827 const allParagraphs = popupContent.querySelectorAll('p.flex-phone.flex-center'); 828 let targetP = null; 829 830 allParagraphs.forEach(p => { 831 const text = p.textContent.trim(); 832 if (text === 'Bandwidth & Antennas' && !targetP) { 833 targetP = p; 834 } else if (text === 'Filters' && !targetP) { 835 targetP = p; 836 } 837 }); 838 839 if (targetP && !document.getElementById('scanner-button-wrapper')) { 840 const scannerLabel = document.createElement('p'); 841 scannerLabel.className = 'flex-phone flex-center'; 842 scannerLabel.textContent = 'Scanner'; 843 844 const scannerContainer = document.createElement('div'); 845 scannerContainer.className = 'flex-container flex-phone flex-center'; 846 scannerContainer.id = 'scanner-button-wrapper'; 847 scannerContainer.style.marginBottom = '10px'; 848 849 scannerContainer.appendChild(ScannerButton); 850 851 popupContent.insertBefore(scannerLabel, targetP); 852 popupContent.insertBefore(scannerContainer, scannerLabel.nextSibling); 853 } 854 } 855 } else { 856 const buttonIms = document.querySelector('.button-ims'); 857 if (buttonIms && ScannerButton) { 858 const newDiv = document.createElement('div'); 859 newDiv.className = 'panel-50 no-bg br-0 h-100 m-0'; 860 newDiv.id = 'ScannerButtonWrapper'; 861 newDiv.appendChild(ScannerButton); 862 buttonIms.parentNode.insertBefore(newDiv, buttonIms); 863 } 864 } 865 866 function toggleScan(isLongPressAction) { 867 const ScanButton = document.getElementById('Scan-on-off'); 868 const isScanOn = ScanButton.getAttribute('data-scan-status') === 'on'; 869 870 const scannerControls = document.getElementById('scanner-controls'); 871 const volumeSlider = document.getElementById('volumeSlider'); 872 const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null; 873 874 if (isLongPressAction) { 875 if (isTuneAuthenticated) { 876 const currentScanStatus = isScanOn; 877 878 if (scannerControls) { 879 scannerControls.parentNode.removeChild(scannerControls); 880 if (volumeSliderParent) { 881 volumeSliderParent.style.display = 'block'; 882 } 883 setCookie('scannerControlsStatus', 'off', 7); 884 } else { 885 // Priority to the remembered state over closure variables 886 if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') { 887 const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit); 888 createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue); 889 } else { 890 const norm = normalizeSensitivity(Sensitivity, SignalStrengthUnit); 891 createScannerControls(norm, ScannerMode, ScanHoldTime); 892 }
893 setCookie('scannerControlsStatus', 'on', 7); 894 } 895 896 if (currentScanStatus) { 897 ScanButton.setAttribute('data-scan-status', 'on'); 898 } else { 899 ScanButton.setAttribute('data-scan-status', 'off'); 900 } 901 902 } else { 903 sendToast('warning', 'Scanner', 'Admin must be logged in to use the autoscan mode!', false, false); 904 } 905 } else { 906 if (isTuneAuthenticated) { 907 if (isScanOn) { 908 sendScan('off'); 909 setCookie('scannerControlsStatus', 'off', 7); 910 } else { 911 sendScan('on'); 912 setCookie('scannerControlsStatus', 'on', 7); 913 } 914 } else { 915 sendToast('warning', 'Scanner', 'Admin must be logged in to use the autoscan mode!', false, false); 916 } 917 } 918 } 919 920 function startPressTimer() { 921 isLongPress = false; 922 pressTimer = setTimeout(() => { 923 isLongPress = true; 924 toggleScan(true); 925 }, 1000); 926 } 927 928 function cancelPressTimer() { 929 clearTimeout(pressTimer); 930 if (!isLongPress) { 931 toggleScan(false); 932 } 933 } 934 935 const ScanButton = document.getElementById('Scan-on-off'); 936 ScanButton.addEventListener('mousedown', startPressTimer); 937 ScanButton.addEventListener('mouseup', cancelPressTimer); 938 939 const scannerControlsStatus = getCookie('scannerControlsStatus'); 940 if (scannerControlsStatus === 'on' && isTuneAuthenticated) { 941 if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') { 942 const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit); 943 createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue); 944 } else { 945 const norm = normalizeSensitivity(Sensitivity, SignalStrengthUnit); 946 createScannerControls(norm, ScannerMode, ScanHoldTime); 947 } 948 } else { 949 const scannerControls = document.getElementById('scanner-controls'); 950 if (scannerControls) { 951 scannerControls.parentNode.removeChild(scannerControls); 952 } 953 } 954 } 955 956 /////////////////////////////////////////////////////////// 957 // Scanner controls 958 /////////////////////////////////////////////////////////// 959 function createScannerControls(Sensitivity, ScannerMode, ScanHoldTime) { 960 const scannerControls = document.createElement('div'); 961 scannerControls.className = "no-bg h-100"; 962 scannerControls.id = "scanner-controls"; 963 scannerControls.style.width = '100%'; 964 scannerControls.style.display = 'flex'; 965 scannerControls.style.justifyContent = 'space-between'; 966 scannerControls.style.marginTop = "0px"; 967 scannerControls.style.marginRight = "0px"; 968 scannerControls.style.position = 'relative'; 969 970 const sensitivityContainer = document.createElement('div'); 971 sensitivityContainer.className = "dropdown"; 972 sensitivityContainer.style.marginRight = "1px"; 973 sensitivityContainer.style.marginLeft = "0px"; 974 sensitivityContainer.style.width = "100%"; 975 sensitivityContainer.style.height = "99%"; 976 sensitivityContainer.style.position = 'relative'; 977 sensitivityContainer.style.borderTopLeftRadius = '15px'; 978 sensitivityContainer.style.borderTopRightRadius = '0px'; 979 sensitivityContainer.style.borderBottomLeftRadius = '15px'; 980 sensitivityContainer.style.borderBottomRightRadius = '0px'; 981 982 const modeContainer = document.createElement('div'); 983 modeContainer.className = "dropdown"; 984 modeContainer.style.marginRight = "1px"; 985 modeContainer.style.marginLeft = "0px"; 986 modeContainer.style.width = "100%"; 987 modeContainer.style.height = "99%"; 988 modeContainer.style.position = 'relative'; 989 modeContainer.style.borderRadius = '0px'; 990 991 let optionsHTML = ` 992 <input type="text" placeholder="${ScannerMode}" title="Scanner Mode" readonly> 993 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 994 <li data-value="normal" class="option">normal</li> 995 `; 996 if (EnableBlacklist) optionsHTML += `<li data-value="blackl
996ist" class="option">blacklist</li>`; 997 if (EnableWhitelist) optionsHTML += `<li data-value="whitelist" class="option">whitelist</li>`; 998 if (EnableSpectrumScan) optionsHTML += `<li data-value="spectrum" class="option">spectrum</li>`; 999 if (EnableSpectrumScanBL) optionsHTML += `<li data-value="spectrumBL" class="option">spectrumBL</li>`; 1000 if (EnableDifferenceScan) optionsHTML += `<li data-value="difference" class="option">difference</li>`; 1001 if (EnableDifferenceScanBL) optionsHTML += `<li data-value="differenceBL" class="option">differenceBL</li>`; 1002 optionsHTML += `</ul>`; 1003 modeContainer.innerHTML = optionsHTML; 1004 1005 const delayContainer = document.createElement('div'); 1006 delayContainer.className = "dropdown"; 1007 delayContainer.style.marginLeft = "0px"; 1008 delayContainer.style.width = "100%"; 1009 delayContainer.style.height = "99%"; 1010 delayContainer.style.position = 'relative'; 1011 delayContainer.style.borderTopLeftRadius = '0px'; 1012 delayContainer.style.borderTopRightRadius = '15px'; 1013 delayContainer.style.borderBottomLeftRadius = '0px'; 1014 delayContainer.style.borderBottomRightRadius = '15px'; 1015 1016 const VolumeSlider = document.getElementById('volumeSlider'); 1017 const VolumeSliderWidth = VolumeSlider ? VolumeSlider.clientWidth : 0; 1018 1019 delayContainer.style.marginRight = VolumeSliderWidth > 300 ? "0px" : "5px"; 1020 1021 const unit = (SignalStrengthUnit || '').toLowerCase(); 1022 1023 if (ScanPE5PVBstatus) { 1024 sensitivityContainer.innerHTML = ` 1025 <input type="text" placeholder="Sensitivity" title="Scanner Sensitivity" readonly> 1026 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 1027 <li data-value="1" class="option">1</li> 1028 <li data-value="2" class="option">2</li> 1029 <li data-value="3" class="option">3</li> 1030 <li data-value="4" class="option">4</li> 1031 <li data-value="5" class="option">5</li> 1032 <li data-value="10" class="option">10</li> 1033 <li data-value="15" class="option">15</li> 1034 <li data-value="20" class="option">20</li> 1035 <li data-value="25" class="option">25</li> 1036 <li data-value="30" class="option">30</li> 1037 </ul> 1038 `; 1039 } else if (unit === 'dbf') { 1040 sensitivityContainer.innerHTML = ` 1041 <input type="text" placeholder="${Sensitivity} dBf" title="Scanner Sensitivity" readonly> 1042 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 1043 <li data-value="1" class="option">1 dBf</li> 1044 <li data-value="2" class="option">2 dBf</li> 1045 <li data-value="3" class="option">3 dBf</li> 1046 <li data-value="4" class="option">4 dBf</li> 1047 <li data-value="5" class="option">5 dBf</li> 1048 <li data-value="10" class="option">10 dBf</li> 1049 <li data-value="15" class="option">15 dBf</li> 1050 <li data-value="20" class="option">20 dBf</li> 1051 <li data-value="25" class="option">25 dBf</li> 1052 <li data-value="30" class="option">30 dBf</li> 1053 <li data-value="35" class="option">35 dBf</li> 1054 <li data-value="40" class="option">
105440 dBf</li> 1055 <li data-value="45" class="option">45 dBf</li> 1056 <li data-value="50" class="option">50 dBf</li> 1057 <li data-value="55" class="option">55 dBf</li> 1058 <li data-value="60" class="option">60 dBf</li> 1059 <li data-value="65" class="option">65 dBf</li> 1060 <li data-value="70" class="option">70 dBf</li> 1061 <li data-value="75" class="option">75 dBf</li> 1062 <li data-value="80" class="option">80 dBf</li> 1063 </ul> 1064 `; 1065 } else if (unit === 'dbuv' || unit === 'dbµv' || unit === 'dbμv') { 1066 sensitivityContainer.innerHTML = ` 1067 <input type="text" placeholder="${Sensitivity} dBuV" title="Scanner Sensitivity" readonly> 1068 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 1069 <li data-value="1" class="option">1 dBuV</li> 1070 <li data-value="2" class="option">2 dBuV</li> 1071 <li data-value="3" class="option">3 dBuV</li> 1072 <li data-value="4" class="option">4 dBuV</li> 1073 <li data-value="5" class="option">5 dBuV</li> 1074 <li data-value="10" class="option">10 dBuV</li> 1075 <li data-value="15" class="option">15 dBuV</li> 1076 <li data-value="20" class="option">20 dBuV</li> 1077 <li data-value="25" class="option">25 dBuV</li> 1078 <li data-value="30" class="option">30 dBuV</li> 1079 <li data-value="35" class="option">35 dBuV</li> 1080 <li data-value="40" class="option">40 dBuV</li> 1081 <li data-value="45" class="option">45 dBuV</li> 1082 <li data-value="50" class="option">50 dBuV</li> 1083 <li data-value="55" class="option">55 dBuV</li> 1084 <li data-value="60" class="option">60 dBuV</li> 1085 <li data-value="65" class="option">65 dBuV</li> 1086 <li data-value="70" class="option">70 dBuV</li> 1087 <li data-value="75" class="option">75 dBuV</li> 1088 <li data-value="80" class="option">80 dBuV</li> 1089 </ul> 1090 `; 1091 } else if (unit === 'dbm') { 1092 sensitivityContainer.innerHTML = ` 1093 <input type="text" placeholder="${Sensitivity} dBm" title="Scanner Sensitivity" readonly> 1094 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 1095 <li data-value="-115" class="option">-115 dBm</li> 1096 <li data-value="-112" class="option">-112 dBm</li> 1097 <li data-value="-113" class="option">-113 dBm</li> 1098 <li data-value="-114" class="option">-114 dBm</li> 1099 <li data-value="-110" class="option">-110 dBm</li> 1100 <li data-value="-105" class="option">-105 dBm</li> 1101 <li data-value="-100" class="option">-100 dBm</li> 1102 <li data-value="-95" class="option">-95 dBm</li> 1103 <li data-value="-90" class="option">-90 dBm</li> 1104 <li data-value="-85" class="option">-85 dBm</li> 1105 <li data-value="-80" class="option">-80 dBm</li> 1106 <li data-value="-75" class="option">-75 dBm</li> 1107 <li data-value="-70" class="option">-70 dBm</li> 1108 <li data-value="-65" class="option">-65 dBm</li> 1109 <li data-value="-60" class="option">-60 dBm</li> 1110 <li data-value="-55" class="option">-55 dBm</li> 1111 <li data-value="-50" class="option">-50 dBm</li> 1112 <li data-value="-45" class="option">-45 dBm</li> 1113 <li data-value="-40" class="option">-40 dBm</li> 1114 </ul> 1115 `; 1116 } else { 1117 sensitivityContainer.innerHTML = ` 1118 <input type="text" placeholder="${Sensitivity}" title="Scanner Sensitivity" readonly> 1119 `; 1120 } 1121 1122 delayContainer.innerHTML = ` 1123 <input type="text" placeholder="${ScanHoldTime} sec." title="Scanhold Time" readonly> 1124 <ul class="options open-top" style="position: absolute; display: none; bottom: 100%; margin-bottom: 5px;"> 1125 <li data-value="1" class="option">1 sec.</li> 1126 <li data-value="2" class="option">2 sec.</li> 1127 <li data-value="3" class="option">3 sec.</li> 1128 <li data-value="4" class="option">4 sec.</li> 1129 <li data-value="5" class="option">5 sec.</li> 1130 <li data-value="7" class="option">7 sec.</li> 1131 <li data-value="10" class="option">10 sec.</li> 1132 <li data-value="15" class="option">15 sec.</li> 1133 <li data-value="20" class="option">20 sec.</li> 1134 <li data-value="30" class="option">30 sec.</li> 1135 </ul> 1136 `; 1137 1138 scannerControls.appendChild(sensitivityContainer); 1139 initializeDropdown(sensitivityContainer, 'Selected Sensitivity:', 'I', Sensitivity, ScannerMode, ScanHoldTime); 1140 1141 if (!ScanPE5PVBstatus) { 1142 modeContainer.style.display = 'block'; 1143 scannerControls.appendChild(modeContainer); 1144 initializeDropdown(modeContainer, 'Selected Mode:', 'M', Sensitivity, ScannerMode, ScanHoldTime); 1145 } 1146 1147 scannerControls.appendChild(delayContainer); 1148 initializeDropdown(delayContainer, 'Selected Delay:', 'K', Sensitivity, ScannerMode, ScanHoldTime); 1149 1150 const volumeSlider = document.getElementById('volumeSlider'); 1151 const volumeSliderParent = volumeSlider ? volumeSlider.parentNode : null; 1152 if (volumeSliderParent) { 1153 volumeSliderParent.style.display = 'none'; 1154 volumeSliderParent.parentNode.insertBefore(scannerControls, volumeSliderParent.
1154nextSibling); 1155 } 1156 } 1157 1158 /////////////////////////////////////////////////////////// 1159 // Dropdown logic 1160 /////////////////////////////////////////////////////////// 1161 function initializeDropdown(container, logPrefix, commandPrefix, Sensitivity, ScannerMode, ScanHoldTime) { 1162 const input = container.querySelector('input'); 1163 const options = container.querySelectorAll('.option'); 1164 const dropdown = container.querySelector('.options'); 1165 1166 input.addEventListener('click', () => { 1167 const isOpen = dropdown.style.display === 'block'; 1168 closeAllDropdowns(); 1169 dropdown.style.display = isOpen ? 'none' : 'block'; 1170 }); 1171 1172 options.forEach(option => { 1173 option.addEventListener('click', () => { 1174 const value = option.getAttribute('data-value'); 1175 input.value = option.textContent.trim(); 1176 input.setAttribute('data-value', value); 1177 dropdown.style.display = 'none'; 1178 1179 // Hole aktuelle Werte direkt aus dem DOM, um veraltete Closure-Werte (wie den Default) zu vermeiden! 1180 const currentSensInput = document.querySelector('input[title="Scanner Sensitivity"]'); 1181 const currentModeInput = document.querySelector('input[title="Scanner Mode"]'); 1182 const currentHoldInput = document.querySelector('input[title="Scanhold Time"]'); 1183 1184 let currentSens = currentSensInput ? currentSensInput.getAttribute('data-value') : Sensitivity; 1185 let currentMode = currentModeInput ? currentModeInput.getAttribute('data-value') : ScannerMode; 1186 let currentHold = currentHoldInput ? currentHoldInput.getAttribute('data-value') : ScanHoldTime; 1187 1188 if (commandPrefix === 'I') { 1189 SendValue(value, currentMode, currentHold); 1190 if ( 1191 SpectrumLimiterValueStatus && 1192 value >= SpectrumLimiterValueStatus && 1193 (ScannerModeStatus === 'spectrum' || 1194 ScannerModeStatus === 'spectrumBL' || 1195 ScannerModeStatus === 'difference' || 1196 ScannerModeStatus === 'differenceBL') 1197 ) { 1198 sendToast('error important', 'Scanner', `Sensitivity must be smaller than SpectrumLimiter (${SpectrumLimiterValueStatus} ${SignalStrengthUnit})!`, false, false); 1199 } 1200 } 1201 if (commandPrefix === 'M') { 1202 SendValue(currentSens, value, currentHold); 1203 ScannerModeStatus = value; 1204 } 1205 if (commandPrefix === 'K') { 1206 SendValue(currentSens, currentMode, value); 1207 } 1208 }); 1209 }); 1210 1211 document.addEventListener('click', (event) => { 1212 if (!container.contains(event.target)) { 1213 dropdown.style.display = 'none'; 1214 } 1215 }); 1216 } 1217 1218 function closeAllDropdowns() { 1219 const allDropdowns = document.querySelectorAll('.dropdown .options'); 1220 allDropdowns.forEach(dropdown => { 1221 dropdown.style.display = 'none'; 1222 }); 1223 } 1224 1225 /////////////////////////////////////////////////////////// 1226 // Admin / Tune authentication check 1227 /////////////////////////////////////////////////////////// 1228 function checkAdminMode() { 1229 const bodyText = document.body.textContent || document.body.innerText; 1230 const isAdminLoggedIn = 1231 bodyText.includes("You are logged in as an administrator.") || 1232 bodyText.includes("You are logged in as an adminstrator."); 1233 const canControlReceiver = 1234 bodyText.includes("You are logged in and can control the receiver."); 1235 1236 // Check if tuner is currently locked to admin 1237 isTunerLocked = !!document.querySelector('.fa-solid.fa-key.pointer.tooltip') || !!document.querySelector('.fa-solid.fa-lock.pointer.tooltip'); 1238 1239 if (isAdminLoggedIn || canControlReceiver) { 1240 console.log("Admin or Tune mode found. Scanner Plugin Authentication successful."); 1241 isTuneAuthenticated = true; 1242 } else { 1243 console.log("No special authentication message found. Authentication failed."); 1244 isTuneAuthenticated = false;
1245 } 1246 1247 if (!isTunerLocked || isTuneAuthenticated) { 1248 isTuningAllowed = true; 1249 } else { 1250 isTuningAllowed = false; 1251 } 1252 1253 // Initialize the Scanner Option dynamic dropdown if logged in as admin 1254 if (isAdminLoggedIn) { 1255 createAdminScannerDropdown(); 1256 } 1257 } 1258 1259 /////////////////////////////////////////////////////////// 1260 // Admin Dynamic Scanner Config Dropdown 1261 /////////////////////////////////////////////////////////// 1262 function createAdminScannerDropdown() { 1263 // Look for the modal content area 1264 const modalContent = document.querySelector('.modal-panel-content'); 1265 if (!modalContent) return; 1266 1267 // Search for the specific paragraph confirming admin login 1268 const paragraphs = Array.from(modalContent.querySelectorAll('p.color-3')); 1269 const adminPara = paragraphs.find(p => 1270 p.textContent.includes("You are logged in as an administrator.") || 1271 p.textContent.includes("You are logged in as an adminstrator.") 1272 ); 1273 1274 if (!adminPara) return; // Admin is not logged in 1275 1276 // If it doesn't exist, create it once 1277 let container = document.getElementById('admin-scanner-dropdown-container'); 1278 if (!container) { 1279 container = document.createElement('div'); 1280 container.id = 'admin-scanner-dropdown-container'; 1281 container.className = 'panel-full flex-center no-bg m-0'; 1282 container.style.flexDirection = 'column'; 1283 container.style.marginTop = '15px'; 1284 container.style.marginBottom = '15px'; 1285 1286 container.innerHTML = ` 1287 <span class="text-bold" style="color: var(--color-4); text-transform: uppercase; font-size: 13px; margin-bottom: 5px;">Scanner Setting</span> 1288 <div class="dropdown" style="width: 200px; position: relative;"> 1289 <input type="text" id="admin-scanner-dropdown-input" placeholder="Default" readonly tabindex="0" style="cursor: pointer; text-align: left; padding-left: 15px;"> 1290 <ul class="options open-bottom" tabindex="-1" style="display: none; position: absolute; z-index: 100; width: 100%; text-align: left;"> 1291 <!-- Options generated dynamically via updateAdminScannerDropdownUI --> 1292 </ul> 1293 </div> 1294 `; 1295 // Insert directly above the admin logged-in text 1296 adminPara.parentNode.insertBefore(container, adminPara); 1297 1298 // Add standard Dropdown Toggle behavior 1299 const dropdownEl = container.querySelector('.dropdown'); 1300 const inputEl = dropdownEl.querySelector('input'); 1301 const optionsList = dropdownEl.querySelector('ul.options'); 1302 1303 inputEl.addEventListener('click', (e) => { 1304 e.stopPropagation(); 1305 const isOpen = optionsList.style.display === 'block'; 1306 1307 // Close all other dropdowns globally to prevent overlap 1308 document.querySelectorAll('.dropdown .options').forEach(ul => { 1309 ul.style.display = 'none'; 1310 }); 1311 1312 optionsList.style.display = isOpen ? 'none' : 'block'; 1313 }); 1314 1315 // Close dropdown when clicking outside 1316 document.addEventListener('click', (e) => { 1317 if (!dropdownEl.contains(e.target)) { 1318 optionsList.style.display = 'none'; 1319 } 1320 }); 1321 } 1322 1323 // Render the options 1324 updateAdminScannerDropdownUI(); 1325 } 1326 1327 function updateAdminScannerDropdownUI() { 1328 const container = document.getElementById('admin-scanner-dropdown-container'); 1329 if (!container) return; 1330 1331 // Hide if there are no additional configs besides default 1332 if (!AvailableScannerConfigs || AvailableScannerConfigs.length <= 1) { 1333 container.style.display = 'none'; 1334 return; 1335 } 1336 1337 container.style.display = 'flex'; 1338 1339 const optionsList = container.querySelector('ul.options'); 1340 const inputEl = container.querySelector('input'); 1341 1342 if (optionsList && inputEl) { 1343 // Set the active displayed value 1344 const displayValue = ActiveScannerConfig === 'default' ? 'Default' : ActiveScannerConfig; 1345 inputEl.value = displayValue; 1346 inputEl.setAttribute('data-value', ActiveScannerConfig); 1347 1348 // Rebuild the list items 1349 optionsList.innerHTML = '';
1350 AvailableScannerConfigs.forEach(conf => { 1351 const li = document.createElement('li'); 1352 li.className = 'option'; 1353 li.tabIndex = 0; 1354 li.setAttribute('data-value', conf); 1355 li.textContent = conf === 'default' ? 'Default' : conf; 1356 1357 li.addEventListener('click', (e) => { 1358 e.stopPropagation(); 1359 inputEl.value = li.textContent; 1360 inputEl.setAttribute('data-value', conf); 1361 optionsList.style.display = 'none'; 1362 1363 console.log(`[Scanner] User selected config: ${conf}`); 1364 1365 // Send command to switch configuration on the server 1366 if (wsSendSocket && wsSendSocket.readyState === WebSocket.OPEN) { 1367 wsSendSocket.send(JSON.stringify({ 1368 type: 'Scanner', 1369 value: { status: 'command', LoadScannerConfig: conf }, 1370 source: clientIp, 1371 target: target 1372 })); 1373 } 1374 }); 1375 optionsList.appendChild(li); 1376 }); 1377 } 1378 } 1379 1380 1381 /////////////////////////////////////////////////////////// 1382 // Mapviewer button (CSV check) 1383 /////////////////////////////////////////////////////////// 1384 window.initializeMapViewerButton = function () { 1385 const buttonId = "Mapviewer"; 1386 1387 const currentDomain = window.location.hostname; 1388 const currentPort = window.location.port ? ':' + window.location.port : ''; 1389 const baseUrl = `${window.location.protocol}//${currentDomain}${currentPort}`; 1390 const csvFileUrl = `${baseUrl}/logs/CSVfilename`; 1391 1392 fetch(csvFileUrl, { cache: "no-cache" }) 1393 .then(response => { 1394 if (!response.ok) { 1395 throw new Error("File does not exist"); 1396 } 1397 return response.text(); 1398 }) 1399 .then(initialContent => { 1400 const checkInterval = setInterval(() => { 1401 if (typeof addIconToPluginPanel === 'function') { 1402 clearInterval(checkInterval); 1403 console.log("addIconToPluginPanel found, adding Map Viewer button..."); 1404 1405 addIconToPluginPanel(buttonId, "Mapviewer", "solid", "globe", "Open URDS Map Viewer"); 1406 1407 setTimeout(() => { 1408 const button = document.getElementById(buttonId); 1409 if (button) { 1410 button.addEventListener('click', () => { 1411 fetch(csvFileUrl, { cache: "no-cache" }) 1412 .then(response => response.text()) 1413 .then(fileContent => { 1414 const trimmedContent = fileContent.trim(); 1415 if (trimmedContent === "NoFileName") { 1416 sendToast( 1417 'warning', 1418 'Scanner', 1419 'No CSV Logfile currently available!', 1420 false, 1421 false 1422 ); 1423 } else { 1424 const url = `https://tef.noobish.eu/logos/URDSMapViewer.html?file=https://cors-proxy.de:13128/${baseUrl}/logs/${trimmedContent}`; 1425 console.log("Opening MapViewer URL:", url); 1426 window.open(url, '_blank'); 1427 } 1428 }) 1429 .catch(error => { 1430 console.error("Error reading CSV file:", error); 1431 sendToast( 1432 'warning', 1433 'Scanner', 1434 'Error reading CSV Logfile!', 1435 false, 1436 false 1437 ); 1438 }); 1439 }); 1440 1441 console.log("â MapViewer button added successfully!"); 1442 } else { 1443 console.error("â MapViewer button was not created. Check if addIconToPluginPanel appended it correctly."); 1444 } 1445 }, 1000); 1446 } 1447 }, 500); 1448 }) 1449 .catch(error => { 1450 if (error.message === "File does not exist") { 1451 return; // silently ignore missing CSV 1452 } 1453 console.error("CSV file fetch error:", error); 1454 }); 1455 }; 1456 1457 if (!window.matchMedia("(pointer: coarse)").matches) { 1458 initializeMapViewerButton(); 1459 initializeValidatorButton(); 1460 } 1461 1462 1463 /////////////////////////////////////////////////////////// 1464 // URDS Log Validator button
1465 /////////////////////////////////////////////////////////// 1466 function initializeValidatorButton() { 1467 const buttonId = "URDSValidator"; 1468 1469 const currentDomain = window.location.hostname; 1470 const currentPort = window.location.port ? ':' + window.location.port : ''; 1471 const baseUrl = `${window.location.protocol}//${currentDomain}${currentPort}`; 1472 const csvFileUrl = `${baseUrl}/logs/CSVfilename`; 1473 1474 fetch(csvFileUrl, { cache: "no-cache" }) 1475 .then(response => { 1476 if (!response.ok) { 1477 throw new Error("File does not exist"); 1478 } 1479 return response.text(); 1480 }) 1481 .then(() => { 1482 const checkInterval = setInterval(() => { 1483 if (typeof addIconToPluginPanel === 'function') { 1484 clearInterval(checkInterval); 1485 console.log("addIconToPluginPanel found, adding URDS Log Validator button..."); 1486 1487 addIconToPluginPanel(buttonId, "Validator", "solid", "file-circle-check", "Open URDS Log Validator"); 1488 1489 setTimeout(() => { 1490 const button = document.getElementById(buttonId); 1491 if (button) { 1492 button.addEventListener('click', () => { 1493 fetch(csvFileUrl, { cache: "no-cache" }) 1494 .then(response => response.text()) 1495 .then(fileContent => { 1496 const trimmedContent = fileContent.trim(); 1497 if (trimmedContent === "NoFileName") { 1498 sendToast( 1499 'warning', 1500 'Scanner', 1501 'No CSV Logfile currently available!', 1502 false, 1503 false 1504 ); 1505 } else { 1506 const fileUrl = `${baseUrl}/logs/${trimmedContent}`; 1507 const url = `https://highpoint.fmdx.org/webtools/urds-log-validator.html?file=${encodeURIComponent('https://cors-proxy.de:13128/' + fileUrl)}`; 1508 console.log("Opening URDS Log Validator URL:", url); 1509 window.open(url, '_blank'); 1510 } 1511 }) 1512 .catch(error => { 1513 console.error("Error reading CSV file:", error); 1514 sendToast( 1515 'warning', 1516 'Scanner', 1517 'Error reading CSV Logfile!', 1518 false, 1519 false 1520 ); 1521 }); 1522 }); 1523 console.log("â URDS Log Validator button added successfully!"); 1524 } else { 1525 console.error("â URDS Log Validator button was not created."); 1526 } 1527 }, 1000); 1528 } 1529 }, 500); 1530 }) 1531 .catch(error => { 1532 if (error.message === "File does not exist") { 1533 return; 1534 } 1535 console.error("CSV file fetch error:", error); 1536 }); 1537 } 1538 1539 /////////////////////////////////////////////////////////// 1540 // NEW: Watcher for Signal Units dropdown
1541 /////////////////////////////////////////////////////////// 1542 function initSignalUnitWatcher() { 1543 const input = document.getElementById('signal-selector-input'); 1544 if (!input) { 1545 console.log('[Scanner] Signal unit selector not found, skipping unit watcher.'); 1546 return; 1547 } 1548 1549 // Initial sync 1550 const initialLabel = input.value || input.placeholder || ''; 1551 SignalStrengthUnit = mapUnitLabelToInternal(initialLabel); 1552 console.log('[Scanner] Initial SignalStrengthUnit from UI:', SignalStrengthUnit); 1553 1554 // Standard JS assignments to input.value do NOT trigger MutationObserver. 1555 // Therefore, we use a lightweight interval to check for changes instantly. 1556 setInterval(() => { 1557 const label = input.value || input.placeholder || ''; 1558 const newUnit = mapUnitLabelToInternal(label); 1559 1560 if (newUnit !== SignalStrengthUnit) { 1561 console.log('[Scanner] Signal unit changed in UI â', SignalStrengthUnit, 'â', newUnit); 1562 SignalStrengthUnit = newUnit; 1563 1564 if (lastInternalSensitivity !== null && lastInternalSensitivity !== '') { 1565 const norm = normalizeSensitivity(lastInternalSensitivity, SignalStrengthUnit); 1566 1567 // Check if scanner controls are currently open 1568 const existing = document.getElementById('scanner-controls'); 1569 if (existing && isTuneAuthenticated) { 1570 existing.parentNode.removeChild(existing); 1571 // Recreate with new units 1572 createScannerControls(norm, lastScannerModeValue, lastScanHoldTimeValue); 1573 // Update the input field text to reflect the new unit 1574 updateDropdownValues(norm, lastScannerModeValue, lastScanHoldTimeValue); 1575 } 1576 } 1577 } 1578 }, 250); 1579 } 1580 1581 /////////////////////////////////////////////////////////// 1582 // DOM ready 1583 /////////////////////////////////////////////////////////// 1584 document.addEventListener('DOMContentLoaded', () => { 1585 setTimeout(() => { 1586 initSignalUnitWatcher(); // <--- wichtig: Einheit aus UI übernehmen + beobachten 1587 BlinkAutoScan(); 1588 checkAdminMode(); 1589 setupSendSocket(); 1590 }, 1000); 1591 }); 1592 1593 /////////////////////////////////////////////////////////// 1594 // Toast wrapper (global safe) 1595 /////////////////////////////////////////////////////////// 1596 function sendToast(type, title, message, autoClose = true, closeOnClick = true) { 1597 if (typeof window.sendToast === "function") { 1598 window.sendToast(type, title, message, autoClose, closeOnClick); 1599 } else { 1600 console.log(`[${title}] ${message}`); 1601 } 1602 } 1603 1604})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.