Merge pull request #2607 from szporwolik/dev_fix_dxcluster_favs_and_layout

[DX Cluster] UI improvements post 2.2 release
This commit is contained in:
Andreas Kristiansen
2025-12-03 08:44:51 +01:00
committed by GitHub
7 changed files with 1893 additions and 1437 deletions

View File

@@ -1,129 +1,82 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Bandmap extends CI_Controller {
function __construct() {
parent::__construct();
$this->load->model('user_model');
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
$this->load->model('bands');
}
function index() {
$this->load->model('cat');
$this->load->model('bands');
$data['radios'] = $this->cat->radios(true);
$data['bands'] = $this->bands->get_user_bands_for_qso_entry();
$footerData = [];
$footerData['scripts'] = [
'assets/js/highcharts/highcharts.js',
'assets/js/highcharts/timeline.js',
'assets/js/highcharts/exporting.js',
'assets/js/highcharts/accessibility.js',
'assets/js/sections/bandmap.js',
];
$data['page_title'] = __("DXCluster");
$this->load->view('interface_assets/header', $data);
$this->load->view('bandmap/index');
$this->load->view('interface_assets/footer', $footerData);
}
function list() {
$this->load->model('cat');
$this->load->model('bands');
$data['radios'] = $this->cat->radios();
$data['radio_last_updated'] = $this->cat->last_updated()->row();
$data['bands'] = $this->bands->get_user_bands_for_qso_entry();
$footerData = [];
$footerData['scripts'] = [
'assets/js/moment.min.js',
'assets/js/datetime-moment.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/datetime-moment.js")),
'assets/js/cat.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/cat.js")),
'assets/js/leaflet/leaflet.geodesic.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/leaflet.geodesic.js")),
'assets/js/leaflet.polylineDecorator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet.polylineDecorator.js")),
'assets/js/leaflet/L.Terminator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/L.Terminator.js")),
'assets/js/sections/callstats.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/callstats.js")),
'assets/js/sections/bandmap_list.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/bandmap_list.js")),
];
// Get Date format
if($this->session->userdata('user_date_format')) {
// If Logged in and session exists
$pageData['custom_date_format'] = $this->session->userdata('user_date_format');
} else {
// Get Default date format from /config/wavelog.php
$pageData['custom_date_format'] = $this->config->item('qso_date_format');
}
switch ($pageData['custom_date_format']) {
case "d/m/y": $pageData['custom_date_format'] = 'DD/MM/YY'; break;
case "d/m/Y": $pageData['custom_date_format'] = 'DD/MM/YYYY'; break;
case "m/d/y": $pageData['custom_date_format'] = 'MM/DD/YY'; break;
case "m/d/Y": $pageData['custom_date_format'] = 'MM/DD/YYYY'; break;
case "d.m.Y": $pageData['custom_date_format'] = 'DD.MM.YYYY'; break;
case "y/m/d": $pageData['custom_date_format'] = 'YY/MM/DD'; break;
case "Y-m-d": $pageData['custom_date_format'] = 'YYYY-MM-DD'; break;
case "M d, Y": $pageData['custom_date_format'] = 'MMM DD, YYYY'; break;
case "M d, y": $pageData['custom_date_format'] = 'MMM DD, YY'; break;
default: $pageData['custom_date_format'] = 'DD/MM/YYYY';
}
$data['page_title'] = __("DXCluster");
$this->load->view('interface_assets/header', $data);
$this->load->view('bandmap/list',$pageData);
$this->load->view('interface_assets/footer', $footerData);
}
// Get user's favorite bands and modes (active ones)
function get_user_favorites() {
session_write_close();
$this->load->model('bands');
$this->load->model('usermodes');
// Get active bands
$activeBands = $this->bands->get_user_bands_for_qso_entry(false); // false = only active
$bandList = [];
if (is_array($activeBands)) {
foreach ($activeBands as $group => $bands) {
if (is_array($bands)) {
foreach ($bands as $band) {
$bandList[] = $band;
}
}
}
}
// Get active modes (user-specific) and categorize them
$activeModes = $this->usermodes->active();
$modeCategories = [
'cw' => false,
'phone' => false,
'digi' => false
];
if ($activeModes) {
foreach ($activeModes as $mode) {
$qrgmode = strtoupper($mode->qrgmode ?? '');
if ($qrgmode === 'CW') {
$modeCategories['cw'] = true;
} elseif ($qrgmode === 'SSB') {
$modeCategories['phone'] = true;
} elseif ($qrgmode === 'DATA') {
$modeCategories['digi'] = true;
}
}
}
header('Content-Type: application/json');
echo json_encode([
'bands' => $bandList,
'modes' => $modeCategories
]);
}
}
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Bandmap extends CI_Controller {
function __construct() {
parent::__construct();
$this->load->model('user_model');
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
$this->load->model('bands');
}
function index() {
$this->load->model('cat');
$this->load->model('bands');
$data['radios'] = $this->cat->radios(true);
$data['bands'] = $this->bands->get_user_bands_for_qso_entry();
$footerData = [];
$footerData['scripts'] = [
'assets/js/highcharts/highcharts.js',
'assets/js/highcharts/timeline.js',
'assets/js/highcharts/exporting.js',
'assets/js/highcharts/accessibility.js',
'assets/js/sections/bandmap.js',
];
$data['page_title'] = __("DXCluster");
$this->load->view('interface_assets/header', $data);
$this->load->view('bandmap/index');
$this->load->view('interface_assets/footer', $footerData);
}
function list() {
$this->load->model('cat');
$this->load->model('bands');
$data['radios'] = $this->cat->radios();
$data['radio_last_updated'] = $this->cat->last_updated()->row();
$data['bands'] = $this->bands->get_user_bands_for_qso_entry();
$footerData = [];
$footerData['scripts'] = [
'assets/js/moment.min.js',
'assets/js/datetime-moment.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/datetime-moment.js")),
'assets/js/cat.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/cat.js")),
'assets/js/leaflet/leaflet.geodesic.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/leaflet.geodesic.js")),
'assets/js/leaflet.polylineDecorator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet.polylineDecorator.js")),
'assets/js/leaflet/L.Terminator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/L.Terminator.js")),
'assets/js/sections/callstats.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/callstats.js")),
'assets/js/sections/bandmap_list.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/bandmap_list.js")),
];
// Get Date format
if($this->session->userdata('user_date_format')) {
// If Logged in and session exists
$pageData['custom_date_format'] = $this->session->userdata('user_date_format');
} else {
// Get Default date format from /config/wavelog.php
$pageData['custom_date_format'] = $this->config->item('qso_date_format');
}
switch ($pageData['custom_date_format']) {
case "d/m/y": $pageData['custom_date_format'] = 'DD/MM/YY'; break;
case "d/m/Y": $pageData['custom_date_format'] = 'DD/MM/YYYY'; break;
case "m/d/y": $pageData['custom_date_format'] = 'MM/DD/YY'; break;
case "m/d/Y": $pageData['custom_date_format'] = 'MM/DD/YYYY'; break;
case "d.m.Y": $pageData['custom_date_format'] = 'DD.MM.YYYY'; break;
case "y/m/d": $pageData['custom_date_format'] = 'YY/MM/DD'; break;
case "Y-m-d": $pageData['custom_date_format'] = 'YYYY-MM-DD'; break;
case "M d, Y": $pageData['custom_date_format'] = 'MMM DD, YYYY'; break;
case "M d, y": $pageData['custom_date_format'] = 'MMM DD, YY'; break;
default: $pageData['custom_date_format'] = 'DD/MM/YYYY';
}
$data['page_title'] = __("DXCluster");
$this->load->view('interface_assets/header', $data);
$this->load->view('bandmap/list',$pageData);
$this->load->view('interface_assets/footer', $footerData);
}
// Get user's active bands and modes/submodes
}

View File

@@ -1,71 +1,186 @@
<?php
class User_Options extends CI_Controller {
function __construct() {
parent::__construct();
$this->load->model('user_model');
$this->load->model('user_options_model');
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
}
public function add_edit_fav() {
$obj = json_decode(file_get_contents("php://input"), true);
foreach($obj as $option_key => $option_value) {
$obj[$option_key]=$this->security->xss_clean($option_value);
}
if ($obj['sat_name'] ?? '' != '') {
$option_name=$obj['sat_name'].'/'.$obj['mode'];
} else {
$option_name=$obj['band'].'/'.$obj['mode'];
}
$this->user_options_model->set_option('Favourite',$option_name, $obj);
$jsonout['success']=1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function get_fav() {
$result=$this->user_options_model->get_options('Favourite');
$jsonout=[];
foreach($result->result() as $options) {
$jsonout[$options->option_name][$options->option_key]=$options->option_value;
}
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function del_fav() {
$result=$this->user_options_model->get_options('Favourite');
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj['option_name'] ?? '' != '') {
$option_name=$this->security->xss_clean($obj['option_name']);
$this->user_options_model->del_option('Favourite',$option_name);
}
$jsonout['success']=1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function dismissVersionDialog() {
$this->user_options_model->set_option('version_dialog', 'confirmed', array('boolean' => 'true'));
}
public function get_qrg_units() {
$qrg_units = [];
foreach($this->session->get_userdata() as $key => $value) {
if (strpos($key, 'qrgunit_') === 0) {
$band = str_replace('qrgunit_', '', $key);
$qrg_units[$band] = $value;
}
}
header('Content-Type: application/json');
echo json_encode($qrg_units);
}
}
?>
<?php
class User_Options extends CI_Controller {
function __construct() {
parent::__construct();
$this->load->model('user_model');
$this->load->model('user_options_model');
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
}
public function add_edit_fav() {
$obj = json_decode(file_get_contents("php://input"), true);
foreach($obj as $option_key => $option_value) {
$obj[$option_key]=$this->security->xss_clean($option_value);
}
if ($obj['sat_name'] ?? '' != '') {
$option_name=$obj['sat_name'].'/'.$obj['mode'];
} else {
$option_name=$obj['band'].'/'.$obj['mode'];
}
$this->user_options_model->set_option('Favourite',$option_name, $obj);
$jsonout['success']=1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function get_fav() {
$result=$this->user_options_model->get_options('Favourite');
$jsonout=[];
foreach($result->result() as $options) {
$jsonout[$options->option_name][$options->option_key]=$options->option_value;
}
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function del_fav() {
$result=$this->user_options_model->get_options('Favourite');
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj['option_name'] ?? '' != '') {
$option_name=$this->security->xss_clean($obj['option_name']);
$this->user_options_model->del_option('Favourite',$option_name);
}
$jsonout['success']=1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function dismissVersionDialog() {
$this->user_options_model->set_option('version_dialog', 'confirmed', array('boolean' => 'true'));
}
/**
* DX Cluster Filter Favorites
*/
public function add_edit_dxcluster_fav() {
$obj = json_decode(file_get_contents("php://input"), true);
if (!$obj || !isset($obj['fav_name']) || trim($obj['fav_name']) === '') {
header('Content-Type: application/json');
echo json_encode(['success' => 0, 'error' => 'Invalid data']);
return;
}
// Sanitize all input
foreach($obj as $option_key => $option_value) {
if (is_array($option_value)) {
$obj[$option_key] = array_map([$this->security, 'xss_clean'], $option_value);
} else {
$obj[$option_key] = $this->security->xss_clean($option_value);
}
}
$option_name = $obj['fav_name'];
unset($obj['fav_name']); // Don't store the name as a value
// Convert arrays to JSON for storage
foreach($obj as $key => $value) {
if (is_array($value)) {
$obj[$key] = json_encode($value);
}
}
$this->user_options_model->set_option('DXClusterFavourite', $option_name, $obj);
$jsonout['success'] = 1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function del_dxcluster_fav() {
$obj = json_decode(file_get_contents("php://input"), true);
if ($obj['option_name'] ?? '' != '') {
$option_name = $this->security->xss_clean($obj['option_name']);
$this->user_options_model->del_option('DXClusterFavourite', $option_name);
}
$jsonout['success'] = 1;
header('Content-Type: application/json');
echo json_encode($jsonout);
}
public function get_qrg_units() {
$qrg_units = [];
foreach($this->session->get_userdata() as $key => $value) {
if (strpos($key, 'qrgunit_') === 0) {
$band = str_replace('qrgunit_', '', $key);
$qrg_units[$band] = $value;
}
}
header('Content-Type: application/json');
echo json_encode($qrg_units);
}
/**
* Combined endpoint: DX Cluster favorites + user bands/modes settings
* Returns both favorites and user configuration in a single request
*/
public function get_dxcluster_user_favs_and_settings() {
session_write_close();
// Get DX Cluster favorites
$result = $this->user_options_model->get_options('DXClusterFavourite');
$favorites = [];
foreach($result->result() as $options) {
$value = $options->option_value;
if (is_string($value) && (strpos($value, '[') === 0 || strpos($value, '{') === 0)) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
$value = $decoded;
}
}
$favorites[$options->option_name][$options->option_key] = $value;
}
// Get user bands and modes
$this->load->model('bands');
$this->load->model('usermodes');
$activeBands = $this->bands->get_user_bands_for_qso_entry(false);
$bandList = [];
if (is_array($activeBands)) {
foreach ($activeBands as $group => $bands) {
if (is_array($bands)) {
foreach ($bands as $band) {
$bandList[] = $band;
}
}
}
}
$activeModes = $this->usermodes->active();
$modeCategories = ['cw' => false, 'phone' => false, 'digi' => false];
$submodes = [];
if ($activeModes) {
foreach ($activeModes as $mode) {
$qrgmode = strtoupper($mode->qrgmode ?? '');
if ($qrgmode === 'CW') {
$modeCategories['cw'] = true;
} elseif ($qrgmode === 'SSB') {
$modeCategories['phone'] = true;
} elseif ($qrgmode === 'DATA') {
$modeCategories['digi'] = true;
}
$submode = !empty($mode->submode) ? $mode->submode : $mode->mode;
if (!empty($submode) && !in_array($submode, $submodes)) {
$submodes[] = $submode;
}
}
}
header('Content-Type: application/json');
echo json_encode([
'favorites' => $favorites,
'userConfig' => [
'bands' => $bandList,
'modes' => $modeCategories,
'submodes' => $submodes
]
]);
}
}
?>

View File

@@ -16,7 +16,7 @@
var lang_bandmap_cat_required = "<?= __("CAT Connection Required"); ?>";
var lang_bandmap_enable_cat = "<?= __("Enable CAT connection to tune the radio"); ?>";
var lang_bandmap_clear_filters = "<?= __("Clear Filters"); ?>";
var lang_bandmap_band_preserved = "<?= __("Band filter preserved (CAT connection is active)"); ?>";
var lang_bandmap_band_preserved = "<?= __("Band filter preserved (band lock is active)"); ?>";
var lang_bandmap_radio = "<?= __("Radio"); ?>";
var lang_bandmap_radio_none = "<?= __("Radio set to None - CAT connection disabled"); ?>";
var lang_bandmap_radio_tuned = "<?= __("Radio Tuned"); ?>";
@@ -28,9 +28,12 @@
var lang_bandmap_sent_to_form = "<?= __("sent to logging form"); ?>";
var lang_bandmap_cat_control = "<?= __("CAT Connection"); ?>";
var lang_bandmap_cat_off = "<?= __("Click to enable CAT connection"); ?>";
var lang_bandmap_cat_on = "<?= __("CAT following radio | Click for frequency marker | Double-click to disable"); ?>";
var lang_bandmap_cat_marker = "<?= __("Frequency marker active | Click to disable marker | Double-click to disable CAT"); ?>";
var lang_bandmap_freq_changed = "<?= __("Frequency filter changed to"); ?>";
var lang_bandmap_cat_on = "<?= __("CAT following radio - Click to disable"); ?>";
var lang_bandmap_cat_lock_off = "<?= __("Click to enable band lock (requires CAT connection)"); ?>";
var lang_bandmap_cat_lock_on = "<?= __("Band lock active - Click to disable"); ?>";
var lang_bandmap_band_lock = "<?= __("Band Lock"); ?>";
var lang_bandmap_band_lock_enabled = "<?= __("Band lock enabled - band filter will track radio band"); ?>";
var lang_bandmap_freq_changed = "<?= __("Band filter changed to"); ?>";
var lang_bandmap_by_transceiver = "<?= __("by transceiver"); ?>";
var lang_bandmap_freq_filter_set = "<?= __("Frequency filter set to"); ?>";
var lang_bandmap_freq_outside = "<?= __("Frequency outside known bands - showing all bands"); ?>";
@@ -40,6 +43,30 @@
var lang_bandmap_modes_applied = "<?= __("Modes applied. Band filter preserved (CAT connection is active)"); ?>";
var lang_bandmap_favorites_applied = "<?= __("Applied your favorite bands and modes"); ?>";
// My Submodes filter translations
var lang_bandmap_my_submodes = "<?= __("My Submodes"); ?>";
var lang_bandmap_submodes_filter_enabled = "<?= __("Submode filter enabled"); ?>";
var lang_bandmap_submodes_filter_disabled = "<?= __("Submode filter disabled - showing all"); ?>";
var lang_bandmap_required_submodes = "<?= __("Required submodes"); ?>";
var lang_bandmap_submodes_settings_hint = "<?= __("Configure in User Settings - Modes"); ?>";
var lang_bandmap_no_submodes_configured = "<?= __("No submodes configured - configure in User Settings - Modes"); ?>";
var lang_bandmap_no_submodes_warning = "<?= __("No submodes enabled in settings - showing all spots"); ?>";
var lang_bandmap_mode_disabled_no_submode = "<?= __("Disabled - no submodes enabled for this mode in User Settings"); ?>";
var lang_bandmap_toggle_cw = "<?= __("Toggle CW mode filter"); ?>";
var lang_bandmap_toggle_digi = "<?= __("Toggle Digital mode filter"); ?>";
var lang_bandmap_toggle_phone = "<?= __("Toggle Phone mode filter"); ?>";
// DX Cluster Filter Favorites translations
var lang_bandmap_filter_favorites = "<?= __("Favorites"); ?>";
var lang_bandmap_save_filters = "<?= __("Save Current Filters..."); ?>";
var lang_bandmap_filter_preset_name = "<?= __("Enter a name for this filter preset:"); ?>";
var lang_bandmap_filter_preset_saved = "<?= __("Filter preset saved"); ?>";
var lang_bandmap_filter_preset_loaded = "<?= __("Filter preset loaded"); ?>";
var lang_bandmap_filter_preset_deleted = "<?= __("Filter preset deleted"); ?>";
var lang_bandmap_delete_filter_confirm = "<?= __("Are you sure to delete this filter preset?"); ?>";
var lang_bandmap_no_filter_presets = "<?= __("No saved filter presets"); ?>";
var lang_bandmap_preset_limit_reached = "<?= __("Maximum of 20 filter presets reached. Please delete some before adding new ones."); ?>";
// Bandmap filter status messages
var lang_bandmap_loading_data = "<?= __("Loading data from DX Cluster"); ?>";
var lang_bandmap_last_fetched = "<?= __("Last fetched for"); ?>";
@@ -140,8 +167,8 @@
var lang_bandmap_mode = "<?= __("Mode"); ?>";
var lang_bandmap_band = "<?= __("Band"); ?>";
// Enable ultra-compact radio status display for bandmap page (tooltip only)
window.CAT_COMPACT_MODE = 'ultra-compact';
// Enable icon-only radio status display for bandmap page (just icon with tooltip)
window.CAT_COMPACT_MODE = 'icon-only';
// Map configuration (matches QSO map settings)
var map_tile_server = '<?php echo $this->optionslib->get_option('option_map_tile_server');?>';
@@ -180,6 +207,9 @@
<a href="https://github.com/wavelog/wavelog/wiki/DXCluster" target="_blank" title="<?= __("DX Cluster Help"); ?>" style="cursor: pointer; padding: 0.5rem; margin: -0.5rem; color: var(--bs-body-color); text-decoration: none; display: inline-flex; align-items: center;">
<i class="fas fa-question-circle" style="font-size: 1.2rem;"></i>
</a>
<button type="button" class="btn btn-sm" id="compactModeToggle" title="<?= __("Compact Mode - Hide/Show Menu"); ?>" style="background: none; border: none; padding: 0.5rem; margin: -0.5rem; color: var(--bs-body-color);">
<i class="fas fa-compress-alt" id="compactModeIcon" style="font-size: 1.2rem;"></i>
</button>
<div id="fullscreenToggleWrapper" style="cursor: pointer; padding: 0.5rem; margin: -0.5rem;">
<button type="button" class="btn btn-sm" id="fullscreenToggle" title="<?= __("Toggle Fullscreen"); ?>" style="background: none; border: none; padding: 0.5rem;">
<i class="fas fa-expand" id="fullscreenIcon" style="font-size: 1.2rem;"></i>
@@ -192,15 +222,20 @@
<!-- Filters Section with darker background and rounded corners -->
<div class="menu-bar">
<!-- Row 1: CAT Connection, Radio Selector, Radio Status (left) | de Continents (right) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- Left: CAT Connection Button -->
<button class="btn btn-sm btn-secondary flex-shrink-0" type="button" id="toggleCatTracking" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="fas fa-radio"></i> <span class="d-none d-sm-inline"><?= __("CAT Connection"); ?></span> <i class="fas fa-info-circle text-muted" style="font-size: 0.75rem;"></i>
</button>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2 compactable-row">
<!-- Left: CAT Connection + Lock Buttons -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleCatTracking" data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?= __("Click to enable CAT connection"); ?>">
<i class="fas fa-radio"></i> <span class="d-none d-sm-inline"><?= __("CAT Connection"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleCatLock" data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?= __("Click to enable band lock (requires CAT connection)"); ?>" disabled>
<i class="fas fa-lock-open"></i>
</button>
</div>
<!-- Radio Selector Dropdown -->
<small class="text-muted me-1 flex-shrink-0 d-none d-md-inline"><?= __("TRX:"); ?></small>
<select class="form-select form-select-sm radios flex-shrink-0" id="radio" name="radio" style="width: auto; min-width: 150px;">
<select class="form-select form-select-sm radios flex-shrink-0" id="radio" name="radio" style="width: auto;">
<option value="0" selected="selected"><?= __("None"); ?></option>
<option value="ws"<?php if ($this->session->userdata('radio') == 'ws') { echo ' selected="selected"'; } ?>><?= __("Live - ") . __("WebSocket (Requires WLGate>=1.1.10)"); ?></option>
<?php foreach ($radios->result() as $row) { ?>
@@ -215,7 +250,7 @@
<div class="d-flex flex-wrap gap-2 align-items-center">
<small class="text-muted me-1 flex-shrink-0"><?= __("de:"); ?></small>
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleAllContinentsFilter" title="<?= __("Select all continents"); ?>"><?= __("World"); ?></button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAllContinentsFilter" title="<?= __("Select all continents"); ?>"><i class="fas fa-globe"></i> <span class="d-none d-sm-inline"><?= __("World"); ?></span></button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAfricaFilter" title="<?= __("Toggle Africa continent filter"); ?>">AF</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAntarcticaFilter" title="<?= __("Toggle Antarctica continent filter"); ?>">AN</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAsiaFilter" title="<?= __("Toggle Asia continent filter"); ?>">AS</button>
@@ -228,7 +263,7 @@
</div>
<!-- Row 2: Advanced Filters, Favorites, Clear Filters | Band Filters (left) and Mode Filters (right) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2 compactable-row">
<!-- Left: Advanced Filters, Favorites, Clear Filters, and Band Filter Buttons -->
<div class="d-flex flex-wrap gap-2 align-items-center">
<!-- Button Group: Advanced Filters + Favorites + Clear Filters -->
@@ -276,6 +311,7 @@
<label class="form-label d-block filter-label-small" for="requiredFlags"><?= __("Required Flags"); ?></label>
<select id="requiredFlags" class="form-select form-select-sm filter-short" name="required_flags" multiple="multiple">
<option value="None" selected><?= __("None"); ?></option>
<option value="mysubmodes"><?= __("My Submodes"); ?></option>
<option value="lotw"><?= __("LoTW User"); ?></option>
<option value="newcontinent"><?= __("New Continent"); ?></option>
<option value="newcountry"><?= __("New Country"); ?></option>
@@ -378,15 +414,22 @@
</div>
</div>
</div>
<!-- Favorites Button (part of button group) -->
<button class="btn btn-sm btn-secondary" type="button" id="toggleFavoritesFilter" title="<?= __("Apply your favorite bands and modes (configured in Band and Mode settings)"); ?>" style="display: none;">
<i class="fas fa-star text-warning"></i>
</button>
<!-- Clear Filters Button (part of button group) -->
<button class="btn btn-sm btn-secondary" type="button" id="clearFiltersButtonQuick" title="<?= __("Clear all filters except De Continent"); ?>">
<i class="fas fa-filter-circle-xmark text-danger"></i>
</button>
</div>
<!-- DX Cluster Filter Favorites Dropdown -->
<div class="dropdown flex-shrink-0">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="dxclusterFavDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="<?= __("Filter Favorites"); ?>">
<i class="fas fa-star text-warning"></i> <span class="d-none d-md-inline"><?= __("Favorites"); ?></span>
</button>
<div class="dropdown-menu" aria-labelledby="dxclusterFavDropdown" style="min-width: 200px;">
<a class="dropdown-item" href="#" id="dxcluster_fav_add"><i class="fas fa-plus-circle text-success me-2"></i><?= __("Save Current Filters..."); ?></a>
<div class="dropdown-divider"></div>
<div id="dxcluster_fav_menu"></div>
</div>
</div>
<!-- Clear Filters Button -->
<button class="btn btn-sm btn-secondary flex-shrink-0" type="button" id="clearFiltersButtonQuick" title="<?= __("Clear all filters except De Continent"); ?>">
<i class="fas fa-filter-circle-xmark text-danger"></i>
</button>
<!-- MF Band -->
<div class="btn-group flex-shrink-0" role="group">
@@ -404,6 +447,10 @@
<button class="btn btn-sm btn-secondary" type="button" id="toggle12mFilter" title="<?= __("Toggle 12m band filter"); ?>">12m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle10mFilter" title="<?= __("Toggle 10m band filter"); ?>">10m</button>
</div>
<!-- 6m Band -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggle6mFilter" title="<?= __("Toggle 6m band filter"); ?>">6m</button>
</div>
<!-- VHF/UHF/SHF Bands -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleVHFFilter" title="<?= __("Toggle VHF bands filter"); ?>">VHF</button>
@@ -418,15 +465,19 @@
<!-- Right: Mode Filter Buttons -->
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleCwFilter" title="<?= __("Toggle CW mode filter"); ?>">CW</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleDigiFilter" title="<?= __("Toggle Digital mode filter"); ?>">Digi</button>
<button class="btn btn-sm btn-secondary" type="button" id="togglePhoneFilter" title="<?= __("Toggle Phone mode filter"); ?>">Phone</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleCwFilter" title="<?= __("Toggle CW mode filter"); ?>"><i class="fas fa-wave-square"></i> <span class="d-none d-sm-inline">CW</span></button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleDigiFilter" title="<?= __("Toggle Digital mode filter"); ?>"><i class="fas fa-keyboard"></i> <span class="d-none d-sm-inline">Digi</span></button>
<button class="btn btn-sm btn-secondary" type="button" id="togglePhoneFilter" title="<?= __("Toggle Phone mode filter"); ?>"><i class="fas fa-microphone"></i> <span class="d-none d-sm-inline">Phone</span></button>
</div>
</div>
</div>
<!-- Row 3: Quick Filters -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2 compactable-row">
<!-- My Submodes Filter Toggle -->
<button class="btn btn-sm btn-secondary flex-shrink-0" type="button" id="toggleMySubmodesFilter" title="<?= __("Loading submodes..."); ?>">
<i class="fas fa-bookmark"></i> <span class="d-none d-lg-inline"><?= __("My Submodes"); ?></span>
</button>
<!-- LoTW Users Button (separate) -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleLotwFilter" title="<?= __("Toggle LoTW User filter"); ?>">
@@ -471,8 +522,8 @@
</div>
</div>
<!-- Row 5: Status Bar (70%) and Search (30%) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- Row 5: Status Bar and Search - always stay together -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2 status-search-row">
<!-- Status Bar - 70% -->
<div style="flex: 1 1 0; min-width: 300px;">
<div class="status-bar">

File diff suppressed because it is too large Load Diff

View File

@@ -81,9 +81,37 @@ $(document).ready(function() {
};
// Global setting for radio status display mode (can be set by pages like bandmap)
// Options: false (card wrapper), 'compact' (no card), 'ultra-compact' (tooltip only)
// Options: false (card wrapper), 'compact' (no card), 'ultra-compact' (icon+name+tooltip), 'icon-only' (icon+tooltip)
window.CAT_COMPACT_MODE = window.CAT_COMPACT_MODE || false;
/**
* Safely dispose of a Bootstrap tooltip without triggering _isWithActiveTrigger errors
* This works around a known Bootstrap bug where disposing during hide animation causes errors
* @param {Element} element - The DOM element with the tooltip
*/
function safeDisposeTooltip(element) {
try {
var tooltipInstance = bootstrap.Tooltip.getInstance(element);
if (tooltipInstance) {
// Set _activeTrigger to empty object to prevent _isWithActiveTrigger error
if (tooltipInstance._activeTrigger) {
tooltipInstance._activeTrigger = {};
}
// Clear any pending timeouts
if (tooltipInstance._timeout) {
clearTimeout(tooltipInstance._timeout);
tooltipInstance._timeout = null;
}
// Dispose without calling hide first
tooltipInstance.dispose();
}
} catch(e) {
// Silently ignore any remaining errors
}
}
// Expose globally for other modules
window.safeDisposeTooltip = safeDisposeTooltip;
function initializeWebSocketConnection() {
try {
// Note: Browser will log WebSocket connection errors to console if server is unreachable
@@ -153,14 +181,19 @@ $(document).ready(function() {
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF - show offline status and skip processing
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
if (window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') {
displayOfflineStatus('cat_disabled');
}
return;
}
}
data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
// Calculate age from timestamp, defaulting to 0 (fresh) if timestamp is missing
if (data.timestamp) {
data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
} else {
data.updated_minutes_ago = 0; // Assume fresh if no timestamp
}
// Cache the radio data
updateCATui(data);
}
@@ -379,8 +412,8 @@ $(document).ready(function() {
* @param {string} reason - Optional reason: 'no_radio' (default) or 'cat_disabled'
*/
function displayOfflineStatus(reason) {
// Display "Working offline" message with tooltip in ultra-compact mode
if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
// Display "Working offline" message with tooltip in ultra-compact/icon-only modes
if (window.CAT_COMPACT_MODE !== 'ultra-compact' && window.CAT_COMPACT_MODE !== 'icon-only') {
return;
}
@@ -390,11 +423,20 @@ $(document).ready(function() {
// Use translation variable if available, fallback to English
var offlineText = typeof lang_cat_working_offline !== 'undefined' ? lang_cat_working_offline : 'Working without CAT connection';
const offlineHtml = '<span id="radio_cat_state" class="text-body" style="display: inline-flex; align-items: center; font-size: 0.875rem;">' +
'<i class="fas fa-unlink text-warning" style="margin-right: 5px;"></i>' +
'<span style="margin-right: 5px;">' + offlineText + '</span>' +
'<i id="radio-status-icon" class="fas fa-info-circle text-muted" style="cursor: help;" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
var offlineHtml;
if (window.CAT_COMPACT_MODE === 'icon-only') {
// Icon-only mode: just the icon with tooltip, styled as button for consistent height
offlineHtml = '<span id="radio_cat_state" class="btn btn-sm btn-secondary" style="display: inline-flex; align-items: center; justify-content: center; cursor: help;">' +
'<i id="radio-status-icon" class="fas fa-unlink text-warning" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
} else {
// Ultra-compact mode: icon + text + info icon
offlineHtml = '<span id="radio_cat_state" class="text-body" style="display: inline-flex; align-items: center; font-size: 0.875rem;">' +
'<i class="fas fa-unlink text-warning" style="margin-right: 5px;"></i>' +
'<span style="margin-right: 5px;">' + offlineText + '</span>' +
'<i id="radio-status-icon" class="fas fa-info-circle text-muted" style="cursor: help;" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
}
let tooltipContent;
if (reason === 'cat_disabled') {
@@ -418,11 +460,15 @@ $(document).ready(function() {
// Initialize tooltip
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
try {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
} catch(e) {
// Ignore tooltip initialization errors
}
}
}
@@ -433,15 +479,16 @@ $(document).ready(function() {
* CAT_COMPACT_MODE options:
* false - Standard mode with card wrapper
* 'compact' - Compact mode without card wrapper
* 'ultra-compact' - Ultra-compact mode showing only tooltip with info
* 'ultra-compact' - Ultra-compact mode showing icon, radio name, and tooltip
* 'icon-only' - Icon-only mode showing just icon with tooltip (for bandmap)
*/
function displayRadioStatus(state, data) {
// On bandmap page, only show radio status when CAT Control is enabled
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF on bandmap
// In ultra-compact mode, show "Working offline" with CAT disabled message
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
// In ultra-compact/icon-only mode, show "Working offline" with CAT disabled message
if (window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') {
// Check if a radio is selected
var selectedRadio = $('.radios option:selected').val();
if (selectedRadio && selectedRadio !== '0') {
@@ -548,7 +595,90 @@ $(document).ready(function() {
var html = baseStyle + icon + content + '</div>';
// Update DOM based on global CAT_COMPACT_MODE setting
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
if (window.CAT_COMPACT_MODE === 'icon-only') {
// Icon-only mode: show just radio icon with tooltip containing all info
var tooltipContent = '';
if (state === 'success') {
var radioName = $('select.radios option:selected').text();
var connectionType = $(".radios option:selected").val() == 'ws' ? lang_cat_live : lang_cat_polling;
tooltipContent = '<b>' + radioName + '</b> (' + connectionType + ')';
// Ensure frequency_formatted exists
var freqFormatted = data.frequency_formatted;
if (!freqFormatted || freqFormatted === 'undefined' || freqFormatted === 'nullkHz') {
freqFormatted = format_frequency(data.frequency);
}
// Add frequency info
if(data.frequency_rx && data.frequency_rx != 0 && data.frequency_rx !== 'undefined') {
// Split operation: show TX and RX separately
if (freqFormatted && freqFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_tx + ':</b> ' + freqFormatted;
}
var rxFormatted = format_frequency(data.frequency_rx);
if (rxFormatted && rxFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_rx + ':</b> ' + rxFormatted;
}
} else {
// Simplex operation: show TX/RX combined
if (freqFormatted && freqFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_tx_rx + ':</b> ' + freqFormatted;
}
}
if(data.mode != null) {
tooltipContent += '<br><b>' + lang_cat_mode + ':</b> ' + data.mode;
}
if(data.power != null && data.power != 0) {
tooltipContent += '<br><b>' + lang_cat_power + ':</b> ' + data.power + 'W';
}
if ($(".radios option:selected").val() != 'ws') {
tooltipContent += '<br><br><i>' + lang_cat_polling_tooltip + '</i>';
}
} else if (state === 'error') {
var radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
tooltipContent = lang_cat_connection_error + ': <b>' + radioName + '</b><br>' + lang_cat_connection_lost;
} else if (state === 'timeout') {
var radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
tooltipContent = lang_cat_connection_timeout + ': <b>' + radioName + '</b><br>' + lang_cat_data_stale;
} else if (state === 'not_logged_in') {
tooltipContent = lang_cat_not_logged_in;
}
var iconOnlyHtml = '<span id="radio_cat_state" class="btn btn-sm btn-secondary" style="display: inline-flex; align-items: center; justify-content: center; cursor: help;">' +
'<i id="radio-status-icon" class="fas fa-radio ' + iconClass + '" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
if (!$('#radio_cat_state').length) {
$('#radio_status').append(iconOnlyHtml);
} else {
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
safeDisposeTooltip(this);
});
$('#radio_cat_state').replaceWith(iconOnlyHtml);
}
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
try {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
} catch(e) {
// Ignore tooltip initialization errors
}
}
// Add blink animation on update
$('#radio_status .fa-radio').addClass('blink-once');
setTimeout(function() {
$('#radio_status .fa-radio').removeClass('blink-once');
}, 600);
} else if (window.CAT_COMPACT_MODE === 'ultra-compact') {
// Ultra-compact mode: show radio icon, radio name, and question mark with tooltip
var tooltipContent = '';
var radioName = '';
@@ -562,17 +692,30 @@ $(document).ready(function() {
connectionType = lang_cat_live;
} else {
connectionType = lang_cat_polling;
} tooltipContent = '<b>' + radioName + '</b> (' + connectionType + ')';
}
tooltipContent = '<b>' + radioName + '</b> (' + connectionType + ')';
// Ensure frequency_formatted exists
var freqFormatted = data.frequency_formatted;
if (!freqFormatted || freqFormatted === 'undefined' || freqFormatted === 'nullkHz') {
freqFormatted = format_frequency(data.frequency);
}
// Add frequency info
if(data.frequency_rx != null && data.frequency_rx != 0) {
tooltipContent += '<br><b>' + lang_cat_tx + ':</b> ' + data.frequency_formatted;
data.frequency_rx_formatted = format_frequency(data.frequency_rx);
if (data.frequency_rx_formatted) {
tooltipContent += '<br><b>' + lang_cat_rx + ':</b> ' + data.frequency_rx_formatted;
if(data.frequency_rx && data.frequency_rx != 0 && data.frequency_rx !== 'undefined') {
// Split operation: show TX and RX separately
if (freqFormatted && freqFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_tx + ':</b> ' + freqFormatted;
}
var rxFormatted = format_frequency(data.frequency_rx);
if (rxFormatted && rxFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_rx + ':</b> ' + rxFormatted;
}
} else {
tooltipContent += '<br><b>' + lang_cat_tx_rx + ':</b> ' + data.frequency_formatted;
// Simplex operation: show TX/RX combined
if (freqFormatted && freqFormatted !== 'undefined') {
tooltipContent += '<br><b>' + lang_cat_tx_rx + ':</b> ' + freqFormatted;
}
}
// Add mode
@@ -611,10 +754,7 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
safeDisposeTooltip(this);
});
$('#radio_cat_state').replaceWith(ultraCompactHtml);
}
@@ -622,11 +762,15 @@ $(document).ready(function() {
// Initialize tooltip with dynamic content
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
try {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
} catch(e) {
// Ignore tooltip initialization errors
}
}
// Add blink animation to radio icon on update
@@ -643,10 +787,7 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
safeDisposeTooltip(this);
});
$('#radio_cat_state').html(html);
}
@@ -658,18 +799,15 @@ $(document).ready(function() {
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
safeDisposeTooltip(this);
});
// Update existing panel content
$('#radio_cat_state .card-body').html(html);
}
}
// Initialize Bootstrap tooltips for any new tooltip elements in the radio panel (except ultra-compact which handles its own)
if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
// Initialize Bootstrap tooltips for any new tooltip elements in the radio panel (except ultra-compact/icon-only which handle their own)
if (window.CAT_COMPACT_MODE !== 'ultra-compact' && window.CAT_COMPACT_MODE !== 'icon-only') {
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
new bootstrap.Tooltip(this);
});
@@ -1007,8 +1145,8 @@ $(document).ready(function() {
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always initialize WebSocket connection
initializeWebSocketConnection();
// In ultra-compact mode, show offline status if CAT Control is disabled
if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
// In ultra-compact/icon-only mode, show offline status if CAT Control is disabled
if ((window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
} else {
@@ -1020,8 +1158,8 @@ $(document).ready(function() {
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always start polling
CATInterval=setInterval(updateFromCAT, CAT_CONFIG.POLL_INTERVAL);
// In ultra-compact mode, show offline status if CAT Control is disabled
if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
// In ultra-compact/icon-only mode, show offline status if CAT Control is disabled
if ((window.CAT_COMPACT_MODE === 'ultra-compact' || window.CAT_COMPACT_MODE === 'icon-only') && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
}

View File

@@ -163,7 +163,7 @@ const SIGNAL_BANDWIDTHS = {
const BAND_GROUPS = {
'MF': ['160m'],
'HF': ['80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m'],
'VHF': ['6m', '4m', '2m', '1.25m'],
'VHF': ['4m', '2m', '1.25m'], // Note: 6m has its own separate button in DX Cluster
'UHF': ['70cm', '33cm', '23cm'],
'SHF': ['13cm', '9cm', '6cm', '3cm', '1.25cm', '6mm', '4mm', '2.5mm', '2mm', '1mm']
};

File diff suppressed because it is too large Load Diff