PageSourceSearch

http://voronezhfm.ru/js/plugins/ButtonPresets/pluginButtonPresets.js

js voronezhfm.ru collected 2026-10-08 01:00:49 UTC 69,733 bytes, 1,690 lines download raw bytes

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.