1/* 2 Preset Buttons v1.3.1 by AAD 3 https://github.com/AmateurAudioDude/FM-DX-Webserver-Plugin-Button-Presets 4*/ 5 6(() => { 7 8////////////////////////////////////////////////// 9 10const bankMenuLocation = 'ims' // ims, bw, ant, eq, top, top-replace, hidden 11const bankMenuPosition = 'after' // before, after 12const bankMenuPaddingLeft = '15' // value in px 13const bankMenuPaddingRight = '0' // value in px 14const bankMenuBorderLeftRadius = true; // true, false 15const bankMenuBorderRightRadius = true; // true, false 16const bankMenuCustomWidth = 'default'; // default, value in px or % 17const bankName = 'Bank'; // dropdown menu name 18const bankQuantity = 4; // total number of banks ranging from 3-8 19const optionHidePresetButtons = false; // true, false 20const optionHideDisplayAll = true; // true, false 21const displayDefaultLogo = true; // true, false 22const enableDefaultLogo = 'unnamed'; // all, named, unnamed 23const infoIcon = true; // true, false 24const defaultPresetData = { 25 values: [87.5, 87.5, 87.5, 87.5, 87.5, 87.5, 87.5, 87.5, 87.5, 87.5], 26 names: ['', '', '', '', '', '', '', '', '', ''], 27 urls: ['', '', '', '', '', '', '', '', '', ''] 28}; 29 30////////////////////////////////////////////////// 31 32/* 33 34 defaultPresetData examples: 35 36 values: [99.7, ... 37 names: ['Example', ... 38 urls: ['/logos/Example.png', ... 39 40 values: [99.7, ... 41 names: ['Example', ... 42 urls: ['https://tef.noobish.eu/logos/EXA/Example.png', ... 43 44*/ 45 46// Initial value 47let bankDisplayAll = false; 48 49// Global variables for other plugins 50pluginButtonPresets = true; 51 52// Create default logo 53let defaultButtonPresetImagePath = ' data:image/png;base64,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'; 54 55// Create a style element 56let styleButtonPresets = document.createElement('style'); 57styleButtonPresets.innerHTML = ` 58/* Frosted glass effect */ 59#plugin-button-presets button, 60.tooltip-presets::after { 61 backdrop-filter: blur(5px); 62 transition: 0.6s ease background-color; 63} 64 65#plugin-button-presets button:hover { 66 transition: 0.3s ease background-color; 67} 68 69#plugin-button-presets button img { 70 transition: transform 0.2s ease, opacity 0.3s ease-out !important; 71 transform: scale(1); 72} 73 74#plugin-button-presets button:hover img { 75 transition: transform 0.2s ease, opacity 0.3s ease-out !important; 76 transform: scale(1.05); 77} 78 79/* Tooltip styling */ 80.tooltip-presets { 81 position: relative; 82 flex-grow: .1; 83 margin: 0 4px; 84} 85 86.tooltip-presets::after { 87 content: attr(data-tooltip); 88 position: absolute; 89 bottom: 120%; 90 left: 50%; 91 transform: translateX(-50%); 92 padding: 5px 25px; 93 background-color: var(--color-3-transparent); 94 border: 2px solid var(--color-3); 95 color: var(--color-text); 96 border-radius: 15px; 97 white-space: nowrap; 98 opacity: 0; 99 visibility: hidden; 100 transition: opacity 0.3s; 101 pointer-events: none; 102 text-align: center; 103 font-size: 14px; 104 z-index: 1; 105 106} 107 108.tooltip-presets:hover::after { 109 opacity: 1; 110 visibility: visible; 111} 112 113.tooltip-presets[data-tooltip=""]::after { 114 display: none; 115} 116 117/* Media Queries */ 118 119/* Preserve bottom margin (panels.css:93) */ 120@media only screen and (min-width: 960px) and (max-height: 860px) { 121 #plugin-button-presets { 122 /* padding-right: 8px; */ /* Not needed for FM-DX Webserver v1.3.5+ */ 123 } 124 125 /* Fix spacing when chat is enabled */ 126 .wrapper-outer #wrapper .flex-container .panel-10.no-bg.m-0 { 127 padding-bottom: 0; 128 } 129 130 .wrapper-outer #wrapper .flex-container .panel-10.no-bg.m-0 .panel-10.no-bg.h-100 { 131 height: 80px !important; 132 } 133} 134 135/* Add spacing below bottom containers */ 136@media only screen and (min-width: 768px) { 137 #plugin-button-presets { 138 margin-top: 20px; 139 margin-bottom: 20px; 140 } 141 142 .button-text { 143 font-size: 14px; 144 } 145} 146 147/* Mobile device spacing */ 148@media only screen and (max-width: 480px) { 149 #plugin-button-presets { 150 margin-bottom: 40px; 151 } 152} 153 154/* Prevent cropped text on mobile */ 155@media only screen and not (min-width: 960px) { 156 #plugin-button-presets button { 157 padding: 4px !important; 158 } 159} 160 161/* Force hide dropdown menu in portrait mode */ 162@media only screen and (max-width: 768px) { 163 #button-presets-bank-dropdown, 164 #button-presets-info-icon-container { 165 display: none !important; 166 } 167} 168 169@media only screen and (min-height: 861px) { 170 .button-presets { 171 margin-top: 20px; 172 } 173} 174 175/* Adjustments for screen width */ 176@media only screen and (max-width: 800px) { 177 .button-presets { 178 margin-bottom: 40px; 179 } 180 181 #button-preset-ps { 182 font-size: 0.8em; 183 } 184} 185 186/* Correct tooltips for mobile devices in portrait mode */ 187@media only screen and (max-width: 480px) { 188 #setFrequencyButton5::after, 189 #setFrequencyButton6::after, 190 #setFrequencyButton7::after, 191 #setFrequencyButton8::after, 192 #setFrequencyButton9::after { 193 top: 120%; 194 bottom: inherit !important; 195 } 196} 197 198/* Import/Export buttons */ 199#plugin-button-presets { 200 position: relative; 201} 202#import-button-preset, 203#export-button-preset { 204 opacity: .48; 205 transition: opacity 0.8s !important; 206} 207#import-button-preset:hover, 208#export-button-preset:hover { 209 opacity: .96; 210 transition: opacity 0.8s !important; 211} 212 213.w-50 { 214 min-width: 50px !important; 215} 216 217/* Move import/export if not enough width */ 218@media only screen and (max-width: 1300px) { 219 #export-button-preset, 220 #import-button-preset {
221 left: -8px; 222 } 223} 224 225/* 30+ button display */ 226#plugin-button-presets.button-presets { 227 display: flex; 228 flex-wrap: wrap; 229} 230 231/* 2 rows of 5 buttons for low resolution */ 232@media screen and (max-width: 728px) { 233 #plugin-button-presets { 234 display: grid !important; 235 padding: 0; 236 grid-template-columns: repeat(5, 16%); 237 @media screen and (max-width: 440px) { 238 padding: 0 60px 0 60px; 239 grid-template-columns: repeat(5, 1fr); 240 } 241 } 242 #plugin-button-presets.button-presets button[id^="setFrequencyButton"] { 243 width: calc(100% - 12px) !important; 244 } 245} 246`; 247// Append the style to the head of the document 248document.head.appendChild(styleButtonPresets); 249 250// 30+ button display 251function bankDisplayAllGap() { 252 if (bankDisplayAll) { 253 const styleElement = document.createElement('style'); 254 styleElement.textContent = ` 255 #plugin-button-presets.button-presets { 256 gap: 8px !important; 257 } 258 `; 259 document.head.appendChild(styleElement); 260 } else { 261 const styleElement = document.createElement('style'); 262 styleElement.textContent = ` 263 #plugin-button-presets.button-presets { 264 gap: 4px !important; 265 } 266 `; 267 document.head.appendChild(styleElement); 268 } 269} 270 271let tooltipFirstLoad = false; 272const DISPLAY_KEY_ButtonPresets = 'buttonPresetsHidden'; 273const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 274 275// Create a container for the buttons 276let buttonContainer = document.createElement("div"); 277buttonContainer.id = "plugin-button-presets"; 278buttonContainer.classList.add('button-presets', 'flex-container', 'flex-center', 'show-phone'); 279buttonContainer.style.display = "flex"; 280buttonContainer.style.flexWrap = "wrap"; 281buttonContainer.style.gap = "4px"; 282buttonContainer.style.marginLeft = "6px"; 283buttonContainer.style.marginRight = "6px"; 284 285// Function to get stored button values, data-ps values, and images from localStorage 286function getStoredData(bank) { 287 const key = `buttonPresets${bank}`; 288 let dataButtonPresets; 289 if (bank === "A") { 290 dataButtonPresets = JSON.parse(localStorage.getItem(key)) || { values: [...defaultPresetData.values], ps: [...defaultPresetData.names], images: [...defaultPresetData.urls] }; 291 } else { 292 dataButtonPresets = JSON.parse(localStorage.getItem(key)) || { values: Array(10).fill(87.5), ps: Array(10).fill(''), images: Array(10).fill('') }; 293 } 294 return dataButtonPresets; 295} 296 297// Function to save button values, data-ps values, and images to localStorage 298function saveToLocalStorage(bank, buttonValues, psValues, buttonImages, tooltipValues) { 299 const sanitizedButtonValues = buttonValues.map(value => value === null ? "" : value); 300 const sanitizedPsValues = psValues.map(value => value === null ? "" : value); 301 const sanitizedButtonImages = buttonImages.map(value => value === null ? "" : value); 302 const sanitizedTooltipValues = tooltipValues.map(value => value === null ? "" : value); 303 304 localStorage.setItem(`buttonPresets${bank}`, JSON.stringify({ 305 values: sanitizedButtonValues, 306 ps: sanitizedPsValues, 307 images: sanitizedButtonImages, 308 tooltips: sanitizedTooltipValues 309 })); 310} 311 312// Default to Bank A 313let currentBank = 'A'; 314 315// Create a custom dropdown menu 316let dropdownContainer = document.createElement('div'); 317dropdownContainer.classList.add('panel-50', 'w-50', 'no-bg', 'h-100', 'm-0', 'dropdown', 'dropdown-up', 'hide-phone'); 318dropdownContainer.id = 'button-presets-bank-dropdown'; // Dropdown ID 319 320let dropdownInput = document.createElement('input'); 321dropdownInput.type = 'text'; 322dropdownInput.placeholder = `${bankName} A`; // Bank text 323dropdownInput.readOnly = true; 324dropdownInput.tabIndex = 0; 325dropdownContainer.appendChild(dropdownInput); 326 327let dropdownOptions = document.createElement('ul'); 328dropdownOptions.classList.add('options', 'open-top'); 329dropdownOptions.tabIndex = -1; 330let bankNames; 331if (bankQuantity >= 3 && bankQuantity <= 8) { 332 bankNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'].slice(0, bankQuantity); 333} else { 334 bankNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'].slice(0, 3); 335}
336bankNames.forEach(bank => { 337 let option = document.createElement('li'); 338 option.classList.add('option'); 339 option.dataset.value = bank; 340 option.tabIndex = 0; 341 option.textContent = `${bankName} ${bank}`; // Bank text 342 dropdownOptions.appendChild(option); 343}); 344dropdownContainer.appendChild(dropdownOptions); 345 346// Toggle dropdown options visibility 347dropdownInput.addEventListener('click', function() { 348 dropdownOptions.classList.toggle('opened'); 349}); 350 351// Update bank and input value when an option is selected 352dropdownOptions.addEventListener('click', function(event) { 353 if (event.target && event.target.matches('li.option')) { 354 currentBank = event.target.dataset.value; 355 dropdownInput.value = `${bankName} ${currentBank}`; // Bank text 356 updateButtons(); // Update the buttons when the bank changes 357 dropdownOptions.classList.remove('opened'); 358 } 359}); 360 361// Function to replace F1-F4 preset buttons 362function replacePresets() { 363 document.addEventListener("DOMContentLoaded", () => { 364 let replaceOriginals; 365 366 const presets = { 367 preset1: { bank: "A", label: `${bankName} A` }, 368 preset2: { bank: "B", label: `${bankName} B` }, 369 preset3: { bank: "C", label: `${bankName} C` }, 370 preset4: { bank: "D", label: `${bankName} D` } 371 }; 372 373 const extraPresets = { 374 preset5: { bank: "E", label: `${bankName} E` }, 375 preset6: { bank: "F", label: `${bankName} F` }, 376 preset7: { bank: "G", label: `${bankName} G` }, 377 preset8: { bank: "H", label: `${bankName} H` } 378 }; 379 380 if (bankMenuLocation === 'top-replace') { 381 replaceOriginals = true; 382 } else { 383 replaceOriginals = false; 384 } 385 386 // Get parent of existing buttons 387 const firstButton = document.querySelector('#dashboard-panel-description .flex-container .flex-center #preset1'); 388 let originalContainer, parentNode; 389 if (firstButton) { 390 originalContainer = firstButton.parentNode; 391 parentNode = originalContainer.parentNode; 392 } 393 394 let newContainer1, newContainer2; 395 if (!replaceOriginals && originalContainer && parentNode) { 396 newContainer1 = document.createElement('div'); 397 newContainer1.style.display = 'flex'; 398 newContainer1.style.flexWrap = 'wrap'; 399 newContainer1.style.maxWidth = originalContainer.style.width || '100%'; 400 parentNode.insertBefore(newContainer1, originalContainer.nextSibling); 401 402 if (bankQuantity === 8) { 403 newContainer2 = document.createElement('div'); 404 newContainer2.style.display = 'flex'; 405 newContainer2.style.flexWrap = 'wrap'; 406 newContainer2.style.maxWidth = originalContainer.style.width || '100%'; 407 parentNode.insertBefore(newContainer2, newContainer1.nextSibling); 408 } 409 } 410 411 Object.entries(presets).forEach(([id, { bank, label }], index) => { 412 const button = document.querySelector(`#${id}.no-bg.color-4.hover-brighten`); 413 414 if (button) { 415 if (replaceOriginals) { 416 // Style 417 button.addEventListener("click", (event) => { 418 event.preventDefault(); 419 event.stopPropagation(); 420 currentBank = bank; 421 updateButtons(); 422 }, true); 423 424 if (bankQuantity === 8 && originalContainer && parentNode && !newContainer1) { 425 newContainer1 = document.createElement('div'); 426 newContainer1.style.display = 'flex'; 427 newContainer1.style.flexWrap = 'wrap'; 428 newContainer1.style.maxWidth = originalContainer.style.width || '100%'; 429 parentNode.insertBefore(newContainer1, originalContainer.nextSibling); 430 } 431 432 if (bankQuantity === 8) { 433 const clonedButton = button.cloneNode(true); 434 const newId = `preset${index + 5}`; 435 // Style 436 clonedButton.id = newId; 437 438 const clonedSpan = clonedButton.querySelector(`#${id}-text`); 439 if (clonedSpan) { 440 clonedSpan.id = `${newId}-text`; 441 clonedSpan.textContent = extraPresets[newId].label; 442 } 443 444 newContainer1.appendChild(clonedButton); 445 446 clonedButton.addEventListener("click", (event) => { 447 event.preventDefault(); 448 event.stopPropagation(); 449 currentBank = extraPresets[newId].bank; 450 updateButtons(); 451 }, true); 452 } 453 } else { 454 const newButton = button.cloneNode(true); 455 const newId = `button-${id}`; 456 // Style 457 newButton.id = newId; 458 newButton.style.height = '64px'; 459 newButton.style.filter = 'hue-rotate(45deg)'; 460 461 const newSpan = newButton.querySelector(`#${id}-text`); 462 if (newSpan) { 463 newSpan.id = `${newId}-text`; 464 newSpan.textContent = label; 465 } 466 467 newContainer1.appendChild(newButton); 468 469 newButton.addEventListener("click", (event) => { 470 event.preventDefault(); 471 event.stopPropagation(); 472 currentBank = bank; 473 updateButtons(); 474 }, true); 475 } 476 } 477 }); 478 479 if (!replaceOriginals && bankQuantity === 8) {
480 Object.entries(extraPresets).forEach(([id, { bank, label }], index) => { 481 const origButton = document.querySelector(`#preset${index + 1}.no-bg.color-4.hover-brighten`); 482 if (origButton) { 483 const extraButton = origButton.cloneNode(true); 484 // Style 485 extraButton.id = id; 486 extraButton.style.height = '64px'; 487 extraButton.style.filter = 'hue-rotate(45deg)'; 488 489 const extraSpan = extraButton.querySelector(`#preset${index + 1}-text`); 490 if (extraSpan) { 491 extraSpan.id = `${id}-text`; 492 extraSpan.textContent = label; 493 } 494 495 newContainer2.appendChild(extraButton); 496 497 extraButton.addEventListener("click", (event) => { 498 event.preventDefault(); 499 event.stopPropagation(); 500 currentBank = bank; 501 updateButtons(); 502 }, true); 503 } 504 }); 505 } 506 507 Object.entries(presets).forEach(([id, { label }]) => { 508 const span = document.querySelector(`#${id}-text`); 509 if (span && replaceOriginals) { 510 const observer = new MutationObserver((mutations, obs) => { 511 mutations.forEach(mutation => { 512 if (mutation.type === "childList" && span.textContent.trim() !== "") { 513 span.textContent = label; 514 obs.disconnect(); 515 } 516 }); 517 }); 518 519 observer.observe(span, { childList: true, subtree: true }); 520 521 if (span.textContent.trim() !== "") { 522 span.textContent = label; 523 observer.disconnect(); 524 } 525 } 526 }); 527 }); 528} 529 530// Insert the dropdown menu before the specified element 531let targetElement; 532if (bankMenuLocation !== 'hidden' && !bankDisplayAll) { 533 if (bankMenuLocation == 'ims') { 534 targetElement = document.querySelector('.panel-50.no-bg.br-0.h-100.m-0.button-ims'); 535 } else if (bankMenuLocation == 'bw') { 536 targetElement = document.querySelector('#data-bw.panel-50'); 537 } else if (bankMenuLocation == 'ant') { 538 targetElement = document.querySelector('#data-ant.panel-50'); 539 } else if (bankMenuLocation == 'eq') { 540 targetElement = document.querySelector('.panel-50.no-bg.br-0.h-100.m-0.button-eq'); 541 } else if (bankMenuLocation == 'top' || bankMenuLocation == 'top-replace') { 542 replacePresets(); 543 } 544} 545 546if (targetElement) { 547 548 dropdownContainer.style.setProperty('margin-left', bankMenuPaddingLeft + 'px', 'important'); 549 dropdownContainer.style.setProperty('margin-right', bankMenuPaddingRight + 'px', 'important'); 550 551 if (!bankMenuBorderLeftRadius) { 552 dropdownContainer.querySelector('input').style.setProperty('border-top-left-radius', '0px'); 553 dropdownContainer.querySelector('input').style.setProperty('border-bottom-left-radius', '0px'); 554 } 555 556 if (!bankMenuBorderRightRadius) { 557 dropdownContainer.querySelector('input').style.setProperty('border-top-right-radius', '0px'); 558 dropdownContainer.querySelector('input').style.setProperty('border-bottom-right-radius', '0px'); 559 } 560 561 if (bankMenuCustomWidth !== 'default') { 562 dropdownContainer.style.width = bankMenuCustomWidth; 563 } 564 565 if (bankMenuPosition == 'before') { 566 targetElement.parentNode.insertBefore(dropdownContainer, targetElement); 567 } else { 568 targetElement.parentNode.insertBefore(dropdownContainer, targetElement.nextSibling); 569 } 570} 571 572// Optional: Hide dropdown menu when clicking outside 573document.addEventListener('click', function(event) { 574 if (!dropdownContainer.contains(event.target)) { 575 dropdownOptions.classList.remove('opened'); 576 } 577}); 578 579// Define the getTooltipValue function outside updateButtons 580function getTooltipValue() { 581 const dataStationNameElement = document.getElementById('data-station-name'); 582 const dataPsElement = document.getElementById('data-ps'); 583 584 // Check if #data-station-name exists and is visible 585 if (dataStationNameElement && dataStationNameElement.offsetParent !== null) { 586 return dataStationNameElement.textContent.trim(); 587 } 588 589 // Fallback to #data-ps if #data-station-name doesn't exist or isn't visible 590 return dataPsElement ? dataPsElement.textContent.trim() : ''; 591} 592 593// Update buttons on orientation change 594function updateButtonsDelayed() { 595 setTimeout(updateButtons, 200); 596 setTimeout(checkImageErrors, 200); 597} 598 599// Update buttons based on the selected bank 600function updateButtons() { 601 buttonContainer.innerHTML = ''; // Clear existing buttons 602 // 30+ button display 603 let buttonRows = 1; 604 605 if (bankDisplayAll) { 606 buttonRows = bankQuantity || 3; 607 } else { 608 buttonRows = 1; 609 } 610 611 for (let iAll = 0; iAll < buttonRows; iAll++) { 612 // 30+ button display 613 if (bankDisplayAll) { 614 if (iAll >= 0 && iAll < bankQuantity) { 615 currentBank = bankNames[iAll]; 616 } else { 617 currentBank = ''; 618 } 619 } 620 621 let storedData = getStoredData(currentBank); 622 let buttonValues = storedData.values; 623 let psValues = storedData.ps; 624 let buttonImages = storedData.images || []; 625 let tooltipValues = storedData.tooltips || []; 626 627 for (let i = 0; i < 10; i++) { 628 (function(index) { 629 let button = document.createElement("button"); 630 button.id = `setFrequencyButton${index}`; 631 button.classList.add('tooltip-presets', 'tooltip-presets-once'); 632 button.setAttribute('data-tooltip', tooltipValues[index] || psValues[index]); // Tooltip uses data-station-name if available, otherwise psValue 633 button.style.minWidth = "60px"; 634 button.style.width = "8%"; 635 button.style.height = "48px"; 636 637 updateButton(button, buttonValues[index], index); 638 639 if (!isIOS) { 640 // Default button handling 641 button.addEventListener('click', function() { 642 let commandInput = document.getElementById('commandinput'); 643 const presetInput = buttonValues[index]; 644 645 if (socket.readyState === WebSocket.OPEN) { 646 socket.send("T" + (Math.round((presetInput).toFixed(3) * 1000))); 647 } 648 checkBankASum(); 649 }); 650 651 button.addEventListener('contextmenu', function(e) { 652 e.preventDefault(); 653 let dataFrequencyElement = document.getElementById('data-frequency'); 654 let dataPsElement = document.getElementById('data-ps'); 655 let dataStationNameElement = document.getElementById('data-station-name'); 656 let dataFrequency = dataFrequencyElement ? dataFrequencyElement.textContent : '87.5'; 657 let dataPs = dataPsElement ? dataPsElement.textContent : ''; 658 let tooltipValue = (dataStationNameElement && dataStationNameElement.offsetParent !== null) ? dataStationNameElement.textContent : dataPs; 659 660 buttonValues[index] = parseFloat(dataFrequency) || 87.5; 661 psValues[index] = dataPs; 662 tooltipValues[index] = tooltipValue; 663 buttonImages[index] = getImageSrc(); 664 updateButton(button, buttonValues[index], index); 665 checkBankASum(); 666 if (typeof sendToast === 'function') { sendToast('info', 'Preset Buttons', `Frequency <strong>${buttonValues[index]} MHz</strong> saved to preset bank <b>${currentBank}</b>, button <b>#${(index + 1)}</b>.`, false, false); } 667 console.log("Preset saved:", buttonValues[index], currentBank, (index + 1)); 668 }); 669 } else { 670 // iOS button handling 671 let longPressTimer; 672 const LONG_PRESS_THRESHOLD = 500; // Long press functionality 673 674 button.addEventListener('mousedown', function(e) { 675 e.preventDefault(); 676 longPressTimer = setTimeout(() => { 677 savePreset(); 678 }, LONG_PRESS_THRESHOLD); 679 }); 680 681 button.addEventListener('mouseup', function(e) { 682 clearTimeout(longPressTimer); 683 if (!e.defaultPrevented) { 684 recallPreset(); 685 } 686 }); 687 688 button.addEventListener('touchstart', function(e) { 689 e.preventDefault(); 690 longPressTimer = setTimeout(() => { 691 savePreset(); 692 }, LONG_PRESS_THRESHOLD); 693 }); 694 695 button.addEventListener('touchend', function(e) { 696 clearTimeout(longPressTimer); 697 if (!e.defaultPrevented) { 698 recallPreset(); 699 } 700 }); 701 702 function savePreset() { 703 let dataFrequencyElement = document.getElementById('data-frequency'); 704 let dataPsElement = document.getElementById('data-ps'); 705 let dataStationNameElement = document.getElementById('data-station-name'); 706 let dataFrequency = dataFrequencyElement ? dataFrequencyElement.textContent : '87.5'; 707 let dataPs = dataPsElement ? dataPsElement.textContent : ''; 708 let tooltipValue = (dataStationNameElement && dataStationNameElement.offsetParent !== null) ? dataStationNameElement.textContent : dataPs; 709 710 buttonValues[index] = parseFloat(dataFrequency) || 87.5; 711 psValues[index] = dataPs; 712 tooltipValues[index] = tooltipValue; 713 buttonImages[index] = getImageSrc(); 714 updateButton(button, buttonValues[index], index); 715 checkBankASum(); 716 if (typeof sendToast === 'function') { sendToast('info', 'Preset Buttons', `Frequency <strong>${buttonValues[index]} MHz</strong> saved to preset bank <b>${currentBank}</b>, button <b>#${(index + 1)}</b>.`, false, false); } 717 console.log("Preset saved:", buttonValues[index], currentBank, (index + 1)); 718 } 719 720 function recallPreset() { 721 let commandInput = document.getElementById('commandinput'); 722 const presetInput = buttonValues[index]; 723 724 if (socket.readyState === WebSocket.OPEN) { 725 socket.send("T" + (Math.round((presetInput).toFixed(3) * 1000))); 726 } 727 checkBankASum(); 728 } 729 } 730 731 // Handle keyboard events for SHIFT + S and SHIFT + R 732 document.addEventListener('keydown', function(e) { 733 let isButtonFocused = document.activeElement === button; 734 735 if (isButtonFocused) { 736 if (e.shiftKey && e.key === 'S') { 737 // SHIFT + S key combination 738 let dataFrequencyElement = document.getElementById('data-frequency'); 739 let dataPsElement = document.getElementById('data-ps'); 740 let dataFrequency = dataFrequencyElement ? dataFrequencyElement.textContent : '87.5'; 741 let dataPs = dataPsElement ? dataPsElement.textContent : ''; 742 743 buttonValues[index] = parseFloat(dataFrequency) || 87.5; 744 psValues[index] = dataPs; 745 buttonImages[index] = getImageSrc(); 746 tooltipValues[index] = getTooltipValue() || ''; // Ensures tooltipValue is not null 747 updateButton(button, buttonValues[index], index); 748 checkBankASum(); 749 } else if (e.shiftKey && e.key === 'R') { 750 // SHIFT + R key combination 751 buttonValues[index] = 87.5; 752 psValues[index] = ''; 753 buttonImages[index] = ''; 754 tooltipValues[index] = ''; // Reset the tooltip value as well 755 updateButton(button, buttonValues[index], index); 756 checkBankASum(); 757 } 758 } 759 }); 760 761 // Handle mouse events 762 button.addEventListener('mousedown', function(e) { 763 if (e.button === 1 || e.ctrlKey || (e.shiftKey && e.button === 0)) { 764 if (e.button === 1 || (e.shiftKey && e.button === 0)) { 765 buttonValues[index] = 87.5; 766 psValues[index] = ''; 767 buttonImages[index] = ''; 768 tooltipValues[index] = ''; // Reset the tooltip value as well 769 } else if (e.ctrlKey) { 770 let dataFrequencyElement = document.getElementById('data-frequency'); 771 let dataPsElement = document.getElementById('data-ps'); 772 let dataFrequency = dataFrequencyElement ? dataFrequencyElement.textContent : '87.5'; 773 let dataPs = dataPsElement ? dataPsElement.textContent : ''; 774 775 buttonValues[index] = parseFloat(dataFrequency) || 87.5; 776 psValues[index] = dataPs; 777 buttonImages[index] = getImageSrc(); 778 tooltipValues[index] = getTooltipValue() || ''; // Ensures tooltipValue is not null 779 } 780 updateButton(button, buttonValues[index], index); 781 checkBankASum(); 782 } 783 }); 784 785 function updateButton(button, value, index) { 786 const psValue = psValues[index] || ''; // Ensure psValue is always used for button text 787 const tooltipValue = tooltipValues[index] || psValue; // Fallback to psValue if tooltipValue is undefined 788 const imageSrc = (buttonImages && buttonImages[index]) || ''; // Ensure imageSrc is a valid string 789 const padding = "6px"; 790 const displayText = `${formatValue(value).trim()}<span id="button-preset-ps" style="display: block; margin-top: -2px; white-space: nowrap; overflow: hidden; font-weight: 500;">${psValue.trim()}</span>`; 791 792 button.style.position = "relative"; // Ensure button is positioned to contain the image 793 button.style.padding = padding; 794 button.style.backgroundColor = 'var(--color-1-transparent)'; 795 796 // Remove existing image if present 797 const existingImg = button.querySelector('img'); 798 if (existingImg) { 799 button.removeChild(existingImg); 800 } 801 802 // Remove existing text if present 803 const existingText = button.querySelector('.button-text'); 804 if (existingText) { 805 button.removeChild(existingText); 806 } 807 808 // Append the image if it exists 809 if (imageSrc) { 810 const img = document.createElement('img'); 811 img.src = imageSrc || defaultButtonPresetImagePath; 812 img.style.position = "absolute"; 813 img.style.top = padding; 814 img.style.left = padding; 815 img.style.right = padding; 816 img.style.bottom = padding; 817 img.style.width = `calc(100% - ${padding} * 2)`;
818 img.style.height = `calc(100% - ${padding} * 2)`; 819 img.style.objectFit = "contain"; 820 img.style.transition = "opacity 0.8s"; 821 img.style.borderRadius = "6px"; 822 button.style.userSelect = 'none'; 823 button.appendChild(img); 824 825 // Create and append the text element 826 const textElement = document.createElement('span'); 827 textElement.className = 'button-text'; 828 textElement.innerHTML = displayText; 829 textElement.style.position = "absolute"; 830 textElement.style.top = padding; 831 textElement.style.left = padding; 832 textElement.style.right = padding; 833 textElement.style.bottom = padding; 834 textElement.style.visibility = "hidden"; 835 textElement.style.color = "var(--color-text)"; 836 textElement.style.fontFamily = window.getComputedStyle(document.getElementById('data-ps')).fontFamily; 837 button.appendChild(textElement); 838 839 // Add hover effect for image and text 840 button.addEventListener('mouseover', function() { 841 button.style.backgroundColor = 'var(--color-4-transparent)'; 842 img.style.opacity = "0.1"; 843 textElement.style.visibility = "visible"; // Show text on hover 844 }); 845 button.addEventListener('mouseout', function() { 846 button.style.backgroundColor = 'var(--color-1-transparent)'; 847 img.style.opacity = "1"; // Reset opacity when not hovered 848 textElement.style.visibility = "hidden"; // Hide text when not hovered 849 }); 850 } else { 851 if (displayDefaultLogo && window.matchMedia("(max-width: 860px) and (orientation: portrait)").matches) { 852 paddingMobile = "0px"; 853 } else { 854 paddingMobile = "-8px"; // Default or landscape mode padding 855 } 856 857 // Event listener for orientation change 858 document.addEventListener("DOMContentLoaded", function () { 859 function debounce(func, wait) { 860 let timeout; 861 return function(...args) { 862 clearTimeout(timeout); 863 timeout = setTimeout(() => func.apply(this, args), wait); 864 }; 865 } 866 867 const debouncedUpdateButtons = debounce(updateButtonsDelayed, 400); 868 869 // Add debounced event listener for orientationchange 870 window.addEventListener('orientationchange', debouncedUpdateButtons); 871 }); 872 873 // Display default logo 874 if (displayDefaultLogo) { 875 const paddingDefaultLogo = paddingMobile; 876 const img = document.createElement('img'); 877 img.src = imageSrc || defaultButtonPresetImagePath; 878 img.style.position = "absolute"; 879 880 if (enableDefaultLogo !== 'all' && (enableDefaultLogo === 'unnamed' && psValues[index]) || (enableDefaultLogo === 'named' && !psValues[index])) { 881 img.style.opacity = "0"; 882 } else { 883 img.style.opacity = "0.12"; 884 885 // Add hover effect for image and text 886 button.addEventListener('mouseover', function() { 887 button.style.backgroundColor = 'var(--color-4-transparent)'; 888 img.style.opacity = "0.08"; 889 textElement.style.visibility = "visible"; // Show text on hover 890 }); 891 button.addEventListener('mouseout', function() { 892 button.style.backgroundColor = 'var(--color-1-transparent)'; 893 img.style.opacity = "0.12"; // Reset opacity when not hovered 894 }); 895 } 896 897 img.style.top = paddingDefaultLogo; 898 img.style.left = paddingDefaultLogo; 899 img.style.right = paddingDefaultLogo; 900 img.style.bottom = paddingDefaultLogo; 901 img.style.width = `calc(100% - ${paddingDefaultLogo} * 2)`;
902 img.style.height = `calc(100% - ${paddingDefaultLogo} * 2)`; 903 img.style.objectFit = "contain"; 904 img.style.transition = "opacity 0.8s"; 905 img.style.borderRadius = "0px"; 906 button.style.userSelect = 'none'; 907 button.appendChild(img); 908 909 img.addEventListener('mousedown', (e) => { 910 if (e.button === 0) { 911 e.preventDefault(); // Prevent the default drag behavior 912 } 913 }); 914 } 915 916 // If no image, ensure text is visible and positioned correctly 917 const textElement = document.createElement('span'); 918 textElement.className = 'button-text'; 919 textElement.innerHTML = displayText; 920 textElement.style.position = "relative"; 921 textElement.style.color = "var(--color-text)"; 922 textElement.style.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.8)'; 923 textElement.style.fontFamily = window.getComputedStyle(document.getElementById('data-ps')).fontFamily; 924 button.appendChild(textElement); 925 926 // Add hover effect for buttons without an image 927 button.addEventListener('mouseover', function() { 928 button.style.backgroundColor = 'var(--color-4-transparent)'; 929 }); 930 button.addEventListener('mouseout', function() { 931 button.style.backgroundColor = 'var(--color-1-transparent)'; 932 }); 933 } 934 935 button.setAttribute('data-tooltip', tooltipValue.trim()); 936 // 30+ button display 937 if (bankDisplayAll) { 938 if (iAll >= 0 && iAll < bankQuantity) { 939 currentBank = bankNames[iAll]; 940 } else { 941 currentBank = ''; 942 } 943 } 944 saveToLocalStorage(currentBank, buttonValues, psValues, buttonImages, tooltipValues); 945 } 946 947 function formatValue(value) { 948 if (value === undefined) { 949 value = 87.5; 950 } 951 952 // 0-27 MHz : 3 decimal places 953 // 27-76 MHz : 2 decimal places 954 // 76-108 MHz : 1 decimal place 955 let fixedValue; 956 957 switch (true) { 958 case (value <= 27): 959 fixedValue = value.toFixed(4); 960 break; 961 case (value > 27 && value < 76): 962 fixedValue = value.toFixed(3); 963 break; 964 case (value >= 76 && value <= 108): 965 fixedValue = value.toFixed(2); 966 break; 967 default: 968 fixedValue = value.toFixed(3); 969 } 970 971 return fixedValue.endsWith('0') ? fixedValue.slice(0, -1) : fixedValue; 972 } 973 974 // Function to determine if the image is local or external 975 function isImageLocal(imageUrl) { 976 try { 977 const imageUrlObj = new URL(imageUrl); 978 const siteOrigin = window.location.origin; 979 return imageUrlObj.origin === siteOrigin; 980 } catch (e) { 981 // Handle the case where the URL might be invalid or not absolute 982 return false; 983 } 984 } 985 986 // Function to get the image source and determine if it’s local or external 987 function getImageSrc() { 988 const logoImg = document.getElementById('station-logo'); 989 if (logoImg && logoImg.src && !logoImg.src.startsWith('data:image/png;base64') && !logoImg.src.includes('default')) { 990 const imageUrl = logoImg.src; 991 if (isImageLocal(imageUrl)) { 992 // Handle local image 993 return new URL(imageUrl).pathname; // Store only the path 994 } else { 995 // Handle external image 996 return imageUrl; // Store the full URL 997 } 998 } 999 return ''; 1000 } 1001 1002 buttonContainer.appendChild(button); 1003 })(i); 1004 } // i 1005 } // iAll 1006 createImportExportButtons(); 1007} 1008 1009// Set default bank to A and update buttons on load 1010currentBank = 'A'; 1011updateButtons(); 1012 1013// Find the container to insert the button container after 1014const container = document.querySelector('.wrapper-outer #wrapper .flex-container') || document.querySelector('#wrapper-outer #wrapper .flex-container'); 1015 1016// Insert the button container after the container 1017if (document.getElementById('rt-container')) { 1018 container.parentNode.insertBefore(buttonContainer, container.nextSibling); 1019} 1020 1021// Function to toggle the visibility of the button container 1022function toggleButtonContainer(statusToast) { 1023 // Check the localStorage value 1024 const isHidden = JSON.parse(localStorage.getItem(DISPLAY_KEY_ButtonPresets)) == true;
1025 if (isHidden) { 1026 if (typeof sendToast === 'function') { 1027 sendToast('info', 'Preset Buttons', 'Preset Buttons hidden.', false, false); 1028 console.log('Button Preset plugin hidden'); 1029 } 1030 let element = document.getElementById('plugin-button-presets'); 1031 element.style.setProperty('display', 'none'); 1032 const buttonPresetsBankDropdown = document.getElementById('button-presets-bank-dropdown'); 1033 if (buttonPresetsBankDropdown) buttonPresetsBankDropdown.style.display = 'none'; 1034 let infoIconContainer = document.getElementById('button-presets-info-icon-container'); 1035 1036 if (infoIconContainer) { 1037 infoIconContainer.style.display = 'none'; 1038 1039 let styleButtonPresetsHide = document.createElement('style'); 1040 styleButtonPresetsHide.innerHTML = ` 1041 /* Preserve bottom margin (panels.css:93) */ 1042 @media only screen and (min-width: 960px) and (max-height: 860px) { 1043 .wrapper-outer #wrapper .flex-container .panel-10.no-bg.m-0 .panel-10.no-bg.h-100 { 1044 height: 120px !important; 1045 } 1046 } 1047 `; 1048 // Append the style to the head of the document 1049 document.head.appendChild(styleButtonPresetsHide); 1050 } 1051 1052 } else { 1053 if (typeof sendToast === 'function' && statusToast) { 1054 sendToast('info', 'Preset Buttons', 'Preset Buttons restored.', false, false); 1055 console.log('Button Preset plugin restored'); 1056 } 1057 let element = document.getElementById('plugin-button-presets'); 1058 element.style.setProperty('display', 'flex'); 1059 1060 if (window.innerWidth >= 768) { 1061 let dropdown = document.getElementById('button-presets-bank-dropdown'); 1062 if (dropdown) { 1063 dropdown.style.display = 'block'; 1064 } 1065 } 1066 1067 let infoIconContainer = document.getElementById('button-presets-info-icon-container'); 1068 if (infoIconContainer) { 1069 infoIconContainer.style.display = 'flex'; 1070 1071 let styleButtonPresetsHide = document.createElement('style'); 1072 styleButtonPresetsHide.innerHTML = ` 1073 /* Preserve bottom margin (panels.css:93) */ 1074 @media only screen and (min-width: 960px) and (max-height: 860px) { 1075 .wrapper-outer #wrapper .flex-container .panel-10.no-bg.m-0 .panel-10.no-bg.h-100 { 1076 height: 80px !important; 1077 } 1078 } 1079 `; 1080 // Append the style to the head of the document 1081 document.head.appendChild(styleButtonPresetsHide); 1082 } 1083 checkBankASum(); 1084 } 1085} 1086 1087// Initial tooltip 1088function oncePresetTooltips() { 1089 let tooltipText; 1090 $('.tooltip-presets-once').hover(function(e) { 1091 // Never display again after first click 1092 if (tooltipFirstLoad == true) { return; } else { tooltipFirstLoad = true; } 1093 $(document).on('mousedown', () => { clearTimeout($(this).data('timeout')); return; }); 1094 if (!document.querySelector('.tooltip-presets-once')) { return; } 1095 if (!/Mobi|Android|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent)) { 1096 tooltipText = ` 1097 <strong><i>Left-click</i></strong> or <strong><i>ENTER</i></strong> to recall the preset.<br> 1098 <strong><i>Right-click</i></strong>, <strong><i>CTRL+click</i></strong>, or <strong><i>SHIFT+S</i></strong> to store the preset.<br> 1099 <strong><i>Middle-click</i></strong> or <strong><i>SHIFT+click</i></strong>, or <strong><i>SHIFT+R</i></strong> to reset the preset.<br> 1100 <strong>Stored presets do not affect other browsers.</strong> 1101 `; 1102 } else { 1103 tooltipText = ` 1104 Tap to recall the preset.<br> 1105 Long press to store the preset.<br> 1106 <strong>Stored presets do not affect other browsers.</strong> 1107 `; 1108 } 1109 // Add a delay of 500 milliseconds before creating and appending the tooltip 1110 $(this).data('timeout', setTimeout(() => { 1111 let tooltip = $('<div class="tooltiptext"></div>').html(tooltipText); 1112 $('body').append(tooltip); 1113 1114 let posX = e.pageX; 1115 let posY = e.pageY; 1116 1117 let tooltipWidth = tooltip.outerWidth(); 1118 let tooltipHeight = tooltip.outerHeight(); 1119 posX -= tooltipWidth / 2; 1120 posY -= tooltipHeight + 10; 1121 tooltip.css({ top: posY, left: posX, opacity: .99 }); // Set opacity to 1 1122 // For touchscreen devices 1123 if ((/Mobi|Android|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent)) && ('ontouchstart' in window || navigator.maxTouchPoints)) { 1124 setTimeout(() => { $('.tooltiptext').remove(); }, 10000); 1125 document.addEventListener('touchstart', function() { setTimeout(() => { $('.tooltiptext').remove(); }, 500); }); 1126 } 1127 }, 500)); 1128 }, function() { 1129 // Clear the timeout if the mouse leaves before the delay completes 1130 clearTimeout($(this).data('timeout')); 1131 $('.tooltiptext').remove(); 1132 }).mousemove(function(e) { 1133 let tooltipWidth = $('.tooltiptext').outerWidth(); 1134 let tooltipHeight = $('.tooltiptext').outerHeight(); 1135 let posX = e.pageX - tooltipWidth / 2; 1136 let posY = e.pageY - tooltipHeight - 10; 1137 1138 $('.tooltiptext').css({ top: posY, left: posX }); 1139 }); 1140} 1141 1142// #################### SIDE BAR MENU SETTINGS #################### // 1143 1144// ********** Display additional options in side menu ********** 1145function AdditionalCheckboxesDisplayAll() { // ########## SHOW ALL PRESET BUTTONS ########## 1146 // Insert HTML after second element with class 'form-group checkbox' 1147 function insertHtmlAfterSecondCheckbox() { 1148 // Select all elements with class 'form-group checkbox' 1149 const checkboxes = document.querySelectorAll('.modal-panel-content .form-group'); 1150 1151 // Check if there are at least two such elements 1152 if (checkboxes.length > 0) { 1153 // Create new HTML element 1154 const newDiv = document.createElement('div'); 1155 newDiv.className = 'form-group'; 1156 newDiv.innerHTML = ` 1157 <div class="switch flex-container flex-phone flex-phone-column flex-phone-center"> 1158 <input type="checkbox" tabindex="0" id="show-all-preset-buttons" aria-label="Show all preset buttons"> 1159 <label for="show-all-preset-buttons" class="tooltip" data-tooltip="Enable to display all preset banks on screen."></label>
1160 <span class="text-smaller text-uppercase text-bold color-4 p-10">Show All Presets</span> 1161 </div> 1162 `; 1163 1164 // Insert new element after last 1165 const lastCheckbox = checkboxes[checkboxes.length - 1]; 1166 lastCheckbox.insertAdjacentElement('afterend', newDiv); 1167 } else { 1168 console.warn('There are less than two elements with class "form-group checkbox".'); 1169 } 1170 } 1171 insertHtmlAfterSecondCheckbox(); 1172 1173 let isDisplayAll = localStorage.getItem("buttonPresetsDisplayAll"); 1174 if (isDisplayAll === "true") { 1175 $("#show-all-preset-buttons").prop("checked", true); 1176 } 1177 1178 $("#show-all-preset-buttons").change(function() { 1179 let isChecked = $(this).is(":checked"); 1180 localStorage.setItem("buttonPresetsDisplayAll", isChecked); 1181 setDisplayAll(); 1182 }); 1183 1184 function setDisplayAll() { 1185 if(localStorage.getItem('buttonPresetsDisplayAll') != 'true') { 1186 currentBank = 'A'; 1187 bankDisplayAll = false; 1188 bankDisplayAllGap(); 1189 toggleButtonContainer(); 1190 updateButtons(); 1191 console.log("buttonPresetsDisplayAll = false"); 1192 } else { 1193 bankDisplayAll = true; 1194 bankDisplayAllGap(); 1195 toggleButtonContainer(); 1196 const buttonPresetsBankDropdown = document.getElementById('button-presets-bank-dropdown'); 1197 if (buttonPresetsBankDropdown) buttonPresetsBankDropdown.style.display = 'none'; 1198 updateButtons(); 1199 console.log("buttonPresetsDisplayAll = true"); 1200 } 1201 checkImageErrors(); 1202 } 1203 1204 // Create a MutationObserver instance 1205 const observerBackgroundImage = new MutationObserver((mutationsList) => { 1206 for (const mutation of mutationsList) { 1207 if (mutation.type === 'attributes' && mutation.attributeName === 'style') { 1208 const bodyElement = document.body; 1209 1210 // Check if background style is set 1211 if (window.getComputedStyle(bodyElement).background !== 'none') { 1212 // Stop observing once background is set 1213 observerBackgroundImage.disconnect(); 1214 1215 // Run function 1216 setDisplayAll(); 1217 } 1218 } 1219 } 1220 }); 1221 // Configuration of observer 1222 const config = { 1223 attributes: true, // Observe attribute changes 1224 attributeFilter: ['style'], // Only observe changes to 'style' attribute 1225 }; 1226 // Start observer 1227 observerBackgroundImage.observe(document.body, config); 1228} 1229 1230// Display additional options in side menu and tooltips 1231if (!optionHideDisplayAll) { 1232 AdditionalCheckboxesDisplayAll(); 1233} 1234 1235// ********** Display additional options in side menu ********** 1236function AdditionalCheckboxesButtonPresets() { 1237 // Insert HTML after second element with class 'form-group checkbox' 1238 function insertHtmlAfterSecondCheckbox() { 1239 // Select all elements with class 'form-group checkbox' 1240 const checkboxes = document.querySelectorAll('.modal-panel-content .form-group'); 1241 1242 // Check if there are at least two such elements 1243 if (checkboxes.length > 0) { 1244 // Create new HTML element 1245 const newDiv = document.createElement('div'); 1246 newDiv.className = 'form-group'; 1247 newDiv.innerHTML = ` 1248 <div class="switch flex-container flex-phone flex-phone-column flex-phone-center"> 1249 <input type="checkbox" tabindex="0" id="hide-preset-buttons" aria-label="Hide preset buttons"> 1250 <label for="hide-preset-buttons" class="tooltip" data-tooltip="Enable if you do not want to use the preset buttons."></label> 1251 <span class="text-smaller text-uppercase text-bold color-4 p-10">Hide Preset Buttons</span> 1252 </div> 1253 `; 1254 1255 // Insert new element after last 1256 const lastCheckbox = checkboxes[checkboxes.length - 1]; 1257 lastCheckbox.insertAdjacentElement('afterend', newDiv); 1258 } else { 1259 console.warn('There are less than two elements with class "form-group checkbox".'); 1260 } 1261 } 1262 insertHtmlAfterSecondCheckbox(); 1263 1264 let isButtonPresetsHidden = localStorage.getItem(DISPLAY_KEY_ButtonPresets); 1265 if (isButtonPresetsHidden === "true") { 1266 $("#hide-preset-buttons").prop("checked", true); 1267 } 1268 1269 $("#hide-preset-buttons").change(function() { 1270 let isChecked = $(this).is(":checked"); 1271 localStorage.setItem(DISPLAY_KEY_ButtonPresets, isChecked); 1272 toggleButtonContainer(true); 1273 }); 1274} 1275 1276// Display additional options in side menu and tooltips 1277if (!optionHidePresetButtons) { 1278 AdditionalCheckboxesButtonPresets(); 1279} 1280 1281oncePresetTooltips(); 1282 1283// Function to check if all preset values in bank A add up to 875 1284function checkBankASum() { 1285 // Fetch values for bank A 1286 const storedData = getStoredData('A'); 1287 const buttonValues = storedData.values; 1288 1289 // Calculate the sum of all preset values 1290 const sum = buttonValues.reduce((acc, value) => acc + value, 0); 1291 const sumDefault = defaultPresetData.values.reduce((accumulator, currentValue) => accumulator + currentValue, 0); 1292 1293 // Check if the sum is equal to 875 1294 if (sum === 875 || sum === sumDefault) { 1295 showInfoIcon(); 1296 } else { 1297 hideInfoIcon(); 1298 } 1299} 1300 1301// Function to execute when the sum is 875 1302function executeInfoCode() { 1303 // Create a new div for the info icon 1304 let infoIconContainer = document.createElement('div'); 1305 infoIconContainer.id = 'button-presets-info-icon-container'; // Assign an ID for later use 1306 infoIconContainer.style.position = 'absolute'; 1307 infoIconContainer.style.bottom = '60px'; 1308 infoIconContainer.style.right = '36px'; 1309 infoIconContainer.style.display = 'flex'; 1310 infoIconContainer.style.flexDirection = 'column'; 1311 infoIconContainer.style.alignItems = 'center'; 1312 infoIconContainer.style.imageRendering = 'auto'; 1313 1314 // Create the FontAwesome info icon 1315 let infoIcon = document.createElement('i'); 1316 infoIcon.id = 'info-icon'; // Assign an ID to the icon itself 1317 infoIcon.classList.add('fa-solid', 'fa-circle-question'); 1318 infoIcon.style.fontSize = '20px'; 1319 infoIcon.style.cursor = 'pointer'; 1320 1321 // Add event listener to show alert when icon is clicked 1322 infoIcon.addEventListener('click', function() { 1323 if (typeof pluginThemedPopup !== 'undefined') { 1324 alert(`<strong>PRESET BUTTONS</strong> 1325 <i>This feature allows you to store up to ${bankQuantity * 10} presets, with 10 presets per bank. 1326 To store a frequency:</i> 1327 1328 <strong><i>Left-click</i></strong> or <strong><i>ENTER</i></strong> to recall the preset. 1329 <strong><i>Right-click</i></strong>, <strong><i>CTRL+click</i></strong>, or <strong><i>SHIFT+S</i></strong> to store the preset. 1330 <strong><i>Middle-click</i></strong>, <strong><i>SHIFT+click</i></strong>, or <strong><i>SHIFT+R</i></strong> to reset the preset. 1331 1332 Use the <b>Bank</b> dropdown menu to select from Banks <i>A</i>, <i>B</i>, or <i>C</i>. 1333 1334 <strong>Stored presets are saved only on the current browser.</strong><br> 1335 `, 'Close'); 1336 } else { 1337 alertButtonPresets(`<strong>PRESET BUTTONS</strong> 1338 <i>This feature allows you to store up to ${bankQuantity * 10} presets, with 10 presets per bank. 1339 To store a frequency:</i> 1340 1341 <strong><i>Left-click</i></strong> or <strong><i>ENTER</i></strong> to recall the preset. 1342 <strong><i>Right-click</i></strong>, <strong><i>CTRL+click</i></strong>, or <strong><i>SHIFT+S</i></strong> to store the preset. 1343 <strong><i>Middle-click</i></strong>, <strong><i>SHIFT+click</i></strong>, or <strong><i>SHIFT+R</i></strong> to reset the preset. 1344 1345 Use the <b>Bank</b> dropdown menu to select from Banks <i>A</i>, <i>B</i>, or <i>C</i>. 1346 1347 <strong>Stored presets are saved only on the current browser.</strong><br> 1348 `, 'Close'); 1349 } 1350 }); 1351 1352 // Append the icon to the container and the container to the body 1353 infoIconContainer.appendChild(infoIcon); 1354 document.body.appendChild(infoIconContainer); 1355} 1356 1357// Function to hide the info icon 1358function hideInfoIcon() { 1359 let infoIconContainer = document.getElementById('button-presets-info-icon-container'); 1360 if (infoIconContainer) { 1361 infoIconContainer.style.display = 'none'; 1362 } 1363} 1364 1365// Function to show the info icon 1366function showInfoIcon() { 1367 let infoIconContainer = document.getElementById('button-presets-info-icon-container'); 1368 if (infoIconContainer) { 1369 infoIconContainer.style.display = 'flex'; 1370 } 1371} 1372 1373// Initialize the info icon and check the bank A presets sum 1374if (infoIcon) { 1375 executeInfoCode(); 1376} 1377 1378checkBankASum(); 1379 1380// Call the toggle function on page load 1381toggleButtonContainer(); 1382 1383// Replace any missing logos with default logo 1384const containerButtonPresets = document.getElementById('plugin-button-presets'); 1385 1386function checkImageErrors() { 1387 if (containerButtonPresets) { 1388 containerButtonPresets.querySelectorAll('img').forEach(img => { 1389 img.onerror = () => { 1390 img.src = defaultButtonPresetImagePath; 1391 img.style.filter = 'grayscale(100%)'; 1392 img.style.maxWidth = '92px'; 1393 img.style.maxHeight = '36px'; 1394 img.style.opacity = '0.5'; 1395 }; 1396 1397 img.addEventListener('mousedown', (e) => { 1398 if (e.button === 0) { 1399 e.preventDefault(); // Prevent the default drag behavior 1400 } 1401 }); 1402 });
1403 } 1404} 1405 1406checkImageErrors(); 1407 1408// Export presets to file 1409function exportLocalStorageToFile() { 1410 const data = {}; 1411 bankNames.forEach(bank => { 1412 const key = `buttonPresets${bank}`; 1413 const value = localStorage.getItem(key); 1414 if (value !== null) { // Check if bank exists in localStorage 1415 data[key] = value; 1416 } 1417 }); 1418 1419 const jsonData = JSON.stringify(data, null, 2); 1420 const blob = new Blob([jsonData], { type: 'application/json' }); 1421 const url = URL.createObjectURL(blob); 1422 1423 const a = document.createElement('a'); 1424 a.href = url; 1425 a.download = 'FM-DX_Webserver_Presets.json'; 1426 a.style.display = 'none'; // Hide the link 1427 document.body.appendChild(a); 1428 a.click(); 1429 document.body.removeChild(a); 1430 1431 URL.revokeObjectURL(url); 1432} 1433 1434// Import presets from file 1435function createFileInputAndImport() { 1436 const fileInput = document.createElement('input'); 1437 fileInput.type = 'file'; 1438 fileInput.accept = '.json'; 1439 fileInput.style.display = 'none'; // Hide the input 1440 1441 document.body.appendChild(fileInput); 1442 1443 fileInput.addEventListener('change', function(event) { 1444 const file = event.target.files[0]; 1445 if (file) { 1446 importLocalStorageFromFile(file); 1447 } 1448 }); 1449 1450 fileInput.click(); 1451 1452 fileInput.remove(); 1453} 1454 1455function importLocalStorageFromFile(file) { 1456 const reader = new FileReader(); 1457 1458 reader.onload = function(event) { 1459 try { 1460 const data = JSON.parse(event.target.result); 1461 1462 bankNames.forEach(bank => { 1463 const key = `buttonPresets${bank}`; 1464 if (data[key]) { 1465 localStorage.setItem(key, data[key]); 1466 } 1467 }); 1468 1469 updateButtons(); 1470 1471 setTimeout(() => { 1472 createImportExportButtons(); 1473 }, 100); 1474 1475 checkImageErrors(); 1476 1477 if (typeof sendToast === 'function') { 1478 sendToast('success', 'Preset Buttons Import', 'Preset data successfully imported.', false, false); 1479 } 1480 console.log('Button Preset localStorage updated successfully'); 1481 } catch (e) { 1482 if (typeof sendToast === 'function') { 1483 sendToast('error', 'Preset Buttons Import', 'Error importing preset data.', false, false); 1484 } 1485 console.error('Unable to import localStorage preset data', e); 1486 } 1487 }; 1488 1489 reader.onerror = function(event) { 1490 console.error('Error reading file:', event); 1491 }; 1492 1493 reader.readAsText(file); 1494} 1495 1496// Create and append import/export buttons 1497function createImportExportButtons() { 1498 // Create the export button 1499 const exportButton = document.createElement('button'); 1500 exportButton.id = 'export-button-preset'; 1501 exportButton.classList.add('tooltip-presets'); 1502 exportButton.setAttribute('data-tooltip', 'Export presets to file'); 1503 exportButton.style.fontSize = '14px'; 1504 exportButton.style.backgroundColor = '#990903'; 1505 exportButton.style.border = 'none'; 1506 exportButton.style.borderRadius = '50%'; 1507 exportButton.style.width = '12px'; 1508 exportButton.style.height = '12px'; 1509 exportButton.style.cursor = 'pointer'; 1510 exportButton.style.position = 'absolute'; 1511 exportButton.style.transform = 'scale(0.8)'; 1512 exportButton.style.right = window.innerHeight > 720 ? '-10px' : '-10px'; // Previousy -10px, -2px 1513 exportButton.style.bottom = '26px'; 1514 if (!/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { 1515 exportButton.style.marginRight = '0'; 1516 } 1517 1518 // Create the import button 1519 const importButton = document.createElement('button'); 1520 importButton.id = 'import-button-preset'; 1521 importButton.classList.add('tooltip-presets'); 1522 importButton.setAttribute('data-tooltip', 'Import presets from file'); 1523 importButton.style.fontSize = '14px'; 1524 importButton.style.backgroundColor = '#036009'; 1525 importButton.style.border = 'none'; 1526 importButton.style.borderRadius = '50%'; 1527 importButton.style.width = '12px'; 1528 importButton.style.height = '12px'; 1529 importButton.style.cursor = 'pointer'; 1530 importButton.style.position = 'absolute'; 1531 importButton.style.transform = 'scale(0.8)'; 1532 importButton.style.right = window.innerHeight > 720 ? '-10px' : '-10px'; // Previousy -10px, -2px 1533 importButton.style.bottom = '10px'; 1534 if (!/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { 1535 importButton.style.marginRight = '0'; 1536 } 1537 1538 // Add click event listeners 1539 importButton.addEventListener('click', createFileInputAndImport); 1540 exportButton.addEventListener('click', exportLocalStorageToFile); 1541 1542 // Add the buttons to the #plugin-button-presets element 1543 const pluginButtonPresets = document.querySelector('#plugin-button-presets'); 1544 if (pluginButtonPresets !== null) { 1545 pluginButtonPresets.appendChild(importButton); 1546 pluginButtonPresets.appendChild(exportButton); 1547 1548 1549 // Add hover effect for the #plugin-button-presets 1550 pluginButtonPresets.addEventListener('mouseover', () => { 1551 importButton.style.display = 'inline-block'; 1552 exportButton.style.display = 'inline-block'; 1553 });
1554 1555 if (!/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { 1556 pluginButtonPresets.addEventListener('mouseout', () => { 1557 importButton.style.display = 'none'; 1558 exportButton.style.display = 'none'; 1559 }); 1560 1561 // Initially hide the buttons 1562 importButton.style.display = 'none'; 1563 exportButton.style.display = 'none'; 1564 } 1565 } 1566} 1567 1568// Call function to create and append import/export buttons 1569createImportExportButtons(); 1570 1571/* 1572 Themed Popups v1.1.2 by AAD 1573 https://github.com/AmateurAudioDude/FM-DX-Webserver-Plugin-Themed-Popups 1574*/ 1575 1576let styleElementButtonPresets = document.createElement('style'); 1577let cssCodeThemedPopupsButtonPresets = ` 1578/* Themed Popups CSS */ 1579.popup-plugin { 1580 position: fixed; 1581 top: 50%; 1582 left: 50%; 1583 transform: translate(-50%, -50%); 1584 background-color: var(--color-2); /* Background */ 1585 color: var(--color-main-bright); /* Text */ 1586 padding: 20px; 1587 border-radius: 10px; 1588 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); 1589 opacity: 0; 1590 transition: opacity 0.3s ease-in; 1591 z-index: 9999; 1592} 1593 1594.popup-plugin-content { 1595 text-align: center; 1596} 1597 1598.popup-plugin button { 1599 margin-top: 10px; 1600} 1601 1602.popup-plugin.open { 1603 opacity: .99; 1604} 1605`; 1606styleElementButtonPresets.appendChild(document.createTextNode(cssCodeThemedPopupsButtonPresets)); 1607document.head.appendChild(styleElementButtonPresets); 1608 1609// Function to create the alert popup 1610function alertButtonPresets(popupMessage, popupButton) { 1611 if (typeof popupButton === 'undefined') { 1612 popupButton = 'OK'; 1613 } 1614 if (!popupOpened) { // Check if a popup is not already open 1615 popup = document.createElement('div'); 1616 popup.classList.add('popup-plugin'); 1617 popup.innerHTML = `<div class="popup-plugin-content">${popupMessage.replace(/\n/g, '<br>')}<button id="popup-plugin-close">${popupButton}</button></div>`; 1618 document.body.appendChild(popup); 1619 1620 let closeButton = popup.querySelector('#popup-plugin-close'); 1621 closeButton.addEventListener('click', closePopup); 1622 1623 popup.addEventListener('click', function(event) { 1624 event.stopPropagation(); // Prevent event propagation 1625 }); 1626 1627 // Trigger the fade-in effect 1628 setTimeout(function() { 1629 popup.classList.add('open'); 1630 popupOpened = true; // Set popupOpened flag to true 1631 blurBackground(true); 1632 }, 10); 1633 } 1634} 1635 1636function blurBackground(status) { 1637 // Blur background 1638 if (status === true) { 1639 if (idModal) { 1640 idModal.style.display = 'block'; 1641 setTimeout(function() { 1642 idModal.style.opacity = '1'; 1643 }, 40); 1644 } 1645 } else { 1646 // Restore background 1647 if (idModal) { 1648 setTimeout(function() { 1649 idModal.style.display = 'none'; 1650 }, 400); 1651 idModal.style.opacity = '0'; 1652 } 1653 } 1654} 1655 1656// Global variables for popup state 1657let popupOpened = false; 1658let popup; 1659 1660let popupPromptOpened = false; 1661let idModal = document.getElementById('myModal'); 1662 1663// Function to close the popup 1664function closePopup(event) { 1665 event.stopPropagation(); // Prevent event propagation 1666 popupOpened = false; // Set popupOpened flag to false 1667 popup.classList.remove('open'); // Fade out 1668 setTimeout(function() { 1669 popup.remove(); 1670 blurBackground(false); 1671 }, 300); // Remove after fade-out transition 1672} 1673 1674// Event listener for ESC key to close popup 1675document.addEventListener('keydown', function(event) { 1676 if (popupOpened && (event.key === 'Escape' || event.key === 'Enter')) { 1677 closePopup(event); 1678 blurBackground(false); 1679 } 1680}); 1681 1682// Event listener for clicks outside the popup to close it 1683document.addEventListener('click', function(event) { 1684 if (popupOpened && !popup.contains(event.target)) { 1685 closePopup(event); 1686 blurBackground(false); 1687 } 1688}); 1689 1690})();
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.