mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
Make OnAir-Widget dynamic
This commit is contained in:
@@ -119,7 +119,15 @@ class Widgets extends CI_Controller {
|
||||
* @return void
|
||||
*/
|
||||
public function on_air($user_slug = "") {
|
||||
// determine theme
|
||||
// Check for nojs parameter (for QRZ.com compatibility)
|
||||
$nojs = $this->input->get('nojs', TRUE) == '1';
|
||||
|
||||
if (!$nojs) {
|
||||
$this->output->set_header('Content-Security-Policy: script-src \'self\' \'unsafe-inline\'');
|
||||
$this->output->set_header('X-Frame-Options: ALLOWALL');
|
||||
$this->output->set_header('Feature-Policy: *');
|
||||
}
|
||||
|
||||
$this->load->model('themes_model');
|
||||
$theme = $this->input->get('theme', TRUE);
|
||||
if ($theme != null) {
|
||||
@@ -132,12 +140,13 @@ class Widgets extends CI_Controller {
|
||||
$data['theme'] = $this->config->item('option_theme');
|
||||
}
|
||||
|
||||
// determine text size
|
||||
$text_size = $this->input->get('text_size', true) ?? 1;
|
||||
|
||||
if (empty($user_slug)) {
|
||||
$data['text_size_class'] = $this->prepare_text_size_css_class($text_size);
|
||||
$data['error'] = __("User slug not specified");
|
||||
$data['user_slug'] = '';
|
||||
$data['nojs'] = $nojs;
|
||||
$this->load->view('widgets/on_air', $data);
|
||||
return;
|
||||
}
|
||||
@@ -148,6 +157,8 @@ class Widgets extends CI_Controller {
|
||||
$data['text_size_class'] = $this->prepare_text_size_css_class($text_size);
|
||||
$data['error'] = __("User slug not specified");
|
||||
$data['error'] = $e->getMessage();
|
||||
$data['user_slug'] = $user_slug;
|
||||
$data['nojs'] = $nojs;
|
||||
$this->load->view('widgets/on_air', $data);
|
||||
return;
|
||||
}
|
||||
@@ -158,6 +169,8 @@ class Widgets extends CI_Controller {
|
||||
if ($widget_options->is_enabled === false) {
|
||||
$data['text_size_class'] = $this->prepare_text_size_css_class($text_size);
|
||||
$data['error'] = __("User has on-air widget disabled");
|
||||
$data['user_slug'] = $user_slug;
|
||||
$data['nojs'] = $nojs;
|
||||
$this->load->view('widgets/on_air', $data);
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +178,6 @@ class Widgets extends CI_Controller {
|
||||
$this->load->model('cat');
|
||||
$query = $this->cat->status_for_user_id($user_id);
|
||||
|
||||
|
||||
if ($query->num_rows() > 0) {
|
||||
$radio_timeout_seconds = $this->get_radio_timeout_seconds();
|
||||
$cat_timeout_interval_minutes = floor($radio_timeout_seconds / 60);
|
||||
@@ -173,11 +185,9 @@ class Widgets extends CI_Controller {
|
||||
$last_seen_days_ago = 999;
|
||||
|
||||
foreach ($query->result() as $radio_data) {
|
||||
// There can be multiple radios online, we need to take into account all of them
|
||||
$radio_updated_ago_minutes = $this->calculate_radio_updated_ago_minutes($radio_data->timestamp);
|
||||
|
||||
if ($radio_updated_ago_minutes > $cat_timeout_interval_minutes) {
|
||||
// Radio was updated too long ago - calculate user's "last seen X days ago" value
|
||||
$mins_per_day = 1440;
|
||||
$radio_last_seen_days_ago = (int)floor($radio_updated_ago_minutes / $mins_per_day);
|
||||
$last_seen_days_ago = min($last_seen_days_ago, $radio_last_seen_days_ago);
|
||||
@@ -186,40 +196,123 @@ class Widgets extends CI_Controller {
|
||||
$radio_obj = new \stdClass;
|
||||
$radio_obj->updated_at = $radio_data->timestamp;
|
||||
$radio_obj->frequency_string = $this->prepare_frequency_string_for_widget($radio_data);
|
||||
$radio_obj->mode = $radio_data->mode ?? '';
|
||||
$radios_online[] = $radio_obj;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($radios_online) > 1 && $widget_options->display_only_most_recent_radio) {
|
||||
// in case only most recent radio should be displayed, use only most recently updated radio as a result
|
||||
usort($radios_online, function($radio_a, $radio_b) {
|
||||
if ($radio_a->updated_at == $radio_b->updated_at) return 0;
|
||||
return ($radio_a->updated_at > $radio_b->updated_at) ? -1 : 1;
|
||||
return ($radio_a->updated_at > $radio_b->updated_at) ? -1 : 1;
|
||||
});
|
||||
|
||||
$radios_online = [$radios_online[0]];
|
||||
}
|
||||
|
||||
// last seen text
|
||||
$last_seen_text = $widget_options->display_last_seen ? $this->prepare_last_seen_text($last_seen_days_ago) : null;
|
||||
|
||||
$data['text_size_class'] = $this->prepare_text_size_css_class($text_size);
|
||||
$data['user_slug'] = $user_slug;
|
||||
$data['nojs'] = $nojs;
|
||||
|
||||
// prepare rest of the data for UI
|
||||
$data['user_callsign'] = strtoupper($user->user_callsign);
|
||||
$data['is_on_air'] = count($radios_online) > 0;
|
||||
$data['radios_online'] = $radios_online;
|
||||
$data['last_seen_text'] = $last_seen_text;
|
||||
|
||||
$this->load->view('widgets/on_air', $data);
|
||||
|
||||
} else {
|
||||
$data['text_size_class'] = $this->prepare_text_size_css_class($text_size);
|
||||
$data['user_callsign'] = strtoupper($user->user_callsign);
|
||||
$data['user_slug'] = $user_slug;
|
||||
$data['nojs'] = $nojs;
|
||||
$data['error'] = __("No CAT interfaced radios found. You need to have at least one radio interface configured.");
|
||||
$data['user_callsign'] = strtoupper($user->user_callsign);
|
||||
$data['is_on_air'] = false;
|
||||
$data['radios_online'] = [];
|
||||
$data['last_seen_text'] = null;
|
||||
$this->load->view('widgets/on_air', $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_air_ajax($user_slug = "") {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($user_slug)) {
|
||||
echo json_encode(['error' => 'User slug not specified']);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $this->get_user_by_slug($user_slug);
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = $user->user_id;
|
||||
$widget_options = $this->get_on_air_widget_options($user_id);
|
||||
|
||||
if ($widget_options->is_enabled === false) {
|
||||
echo json_encode(['error' => 'User has on-air widget disabled']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load->model('cat');
|
||||
$query = $this->cat->status_for_user_id($user_id);
|
||||
|
||||
if ($query->num_rows() > 0) {
|
||||
$radio_timeout_seconds = $this->get_radio_timeout_seconds();
|
||||
$cat_timeout_interval_minutes = floor($radio_timeout_seconds / 60);
|
||||
$radios_online = [];
|
||||
$last_seen_days_ago = 999;
|
||||
|
||||
foreach ($query->result() as $radio_data) {
|
||||
$radio_updated_ago_minutes = $this->calculate_radio_updated_ago_minutes($radio_data->timestamp);
|
||||
|
||||
if ($radio_updated_ago_minutes > $cat_timeout_interval_minutes) {
|
||||
$mins_per_day = 1440;
|
||||
$radio_last_seen_days_ago = (int)floor($radio_updated_ago_minutes / $mins_per_day);
|
||||
$last_seen_days_ago = min($last_seen_days_ago, $radio_last_seen_days_ago);
|
||||
} else {
|
||||
$radio_obj = new \stdClass;
|
||||
$radio_obj->updated_at = $radio_data->timestamp;
|
||||
$radio_obj->frequency_string = $this->prepare_frequency_string_for_widget($radio_data);
|
||||
$radio_obj->mode = $radio_data->mode ?? '';
|
||||
$radios_online[] = $radio_obj;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($radios_online) > 1 && $widget_options->display_only_most_recent_radio) {
|
||||
usort($radios_online, function($radio_a, $radio_b) {
|
||||
if ($radio_a->updated_at == $radio_b->updated_at) return 0;
|
||||
return ($radio_a->updated_at > $radio_b->updated_at) ? -1 : 1;
|
||||
});
|
||||
$radios_online = [$radios_online[0]];
|
||||
}
|
||||
|
||||
$last_seen_text = $widget_options->display_last_seen ? $this->prepare_last_seen_text($last_seen_days_ago) : null;
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'user_callsign' => strtoupper($user->user_callsign),
|
||||
'is_on_air' => count($radios_online) > 0,
|
||||
'radios_online' => $radios_online,
|
||||
'last_seen_text' => $last_seen_text,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'user_callsign' => strtoupper($user->user_callsign),
|
||||
'is_on_air' => false,
|
||||
'radios_online' => [],
|
||||
'last_seen_text' => null,
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<!--
|
||||
|
||||
This is an On-Air widget to place in your QRZ.com Bio or somewhere else.
|
||||
This is a DYNAMIC On-Air widget to place in your QRZ.com Bio or somewhere else.
|
||||
|
||||
To use this widget insert this Element:
|
||||
For normal use with JavaScript:
|
||||
<iframe name="iframe" src="[YOUR WAVELOG URL]/widgets/on_air/[PUBLIC SLUG]" height="240" width="640" frameborder="0" align="top"></iframe>
|
||||
|
||||
<iframe name="iframe" src="[YOUR WAVELOG URL]/widgets/on_air/[PUBLIC SLUG]" height="240" width="640" frameborder="0" align="top"></iframe> -->
|
||||
For QRZ.com compatibility (using QRZ.com auto-refresh):
|
||||
<iframe name="iframe" src="[YOUR WAVELOG URL]/widgets/on_air/[PUBLIC SLUG]?nojs=1" height="140" width="640" frameborder="0" align="top" data-refresh="60"></iframe>
|
||||
|
||||
The widget automatically detects the nojs=1 parameter and serves a JavaScript-free version that works in QRZ.com's sandbox environment. QRZ.com's lazyLoader will automatically refresh every 60 seconds using the data-refresh attribute.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?php echo $language['code']; ?>">
|
||||
@@ -14,11 +18,15 @@ To use this widget insert this Element:
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<?php if (isset($nojs) && $nojs): ?>
|
||||
<!-- No auto-refresh for QRZ.com compatibility - blocked by sandbox -->
|
||||
<?php endif; ?>
|
||||
|
||||
<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/<?php echo $theme; ?>/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/<?php echo $theme; ?>/overrides.css">
|
||||
<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/general.css">
|
||||
|
||||
<title><?= "Wavelog On-Air widget"; ?></title>
|
||||
<title><?= "Wavelog Dynamic On-Air widget"; ?></title>
|
||||
<style>
|
||||
.widget.container {
|
||||
max-width: none;
|
||||
@@ -61,10 +69,71 @@ To use this widget insert this Element:
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.refresh-indicator {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
<?php if (!isset($nojs) || !$nojs): ?>
|
||||
.refresh-indicator.updating {
|
||||
opacity: 1;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.on-air {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.status-indicator.off-air {
|
||||
background-color: #6c757d;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
25%, 75% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.frequency-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.mode-band {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 0.8em;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php if (isset($nojs) && $nojs): ?>
|
||||
<!-- No-JavaScript mode for QRZ.com compatibility -->
|
||||
<div class="widget container d-flex">
|
||||
<div class="left-column">
|
||||
<img class="widgetLogo" src="<?php echo base_url(); ?>assets/logo/<?php echo $this->optionslib->get_logo('header_logo', $theme); ?>.png" alt="Logo" />
|
||||
@@ -72,28 +141,100 @@ To use this widget insert this Element:
|
||||
<?php if (!isset($error)) { ?>
|
||||
<div class="right-column">
|
||||
<div class="top-right">
|
||||
<p class="<?= $text_size_class ?>">
|
||||
<?php
|
||||
if ($is_on_air === true) {
|
||||
printf("%s is ON-AIR", $user_callsign);
|
||||
} else {
|
||||
printf("%s is OFF-AIR", $user_callsign);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<div>
|
||||
<span class="status-indicator <?php echo $is_on_air ? 'on-air' : 'off-air'; ?>"></span>
|
||||
<span class="<?= $text_size_class ?>">
|
||||
<?php
|
||||
if ($is_on_air === true) {
|
||||
printf("%s ON-AIR", $user_callsign);
|
||||
} else {
|
||||
printf("%s OFF-AIR", $user_callsign);
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-right mt-3">
|
||||
<div class="bottom-right mt-2">
|
||||
<?php if ($is_on_air === true) { ?>
|
||||
<?php foreach ($radios_online as $radio_data) { ?>
|
||||
<p class="<?= $text_size_class ?>">
|
||||
<?= $radio_data->frequency_string; ?>
|
||||
</p>
|
||||
<div class="frequency-info mb-2">
|
||||
<span class="frequency-display <?= $text_size_class ?>">
|
||||
<?= htmlspecialchars($radio_data->frequency_string); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php } else if ($last_seen_text !== null) { ?>
|
||||
<div class="<?= $text_size_class ?> text-muted">
|
||||
<?= htmlspecialchars($last_seen_text); ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="last-updated mt-2">
|
||||
<small>Updated: <?= date('H:i:s'); ?> (auto-refreshed every 60s)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="right-column">
|
||||
<div class="top-right">
|
||||
<div>
|
||||
<span class="status-indicator off-air"></span>
|
||||
<span class="<?= $text_size_class ?>"><?= __("Error") ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-right mt-2">
|
||||
<div class="<?= $text_size_class ?> text-danger">
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<div class="last-updated mt-2">
|
||||
<small>Updated: <?= date('H:i:s'); ?> (auto-refreshed by QRZ.com every 60s)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Normal JavaScript-enabled mode -->
|
||||
<div class="widget container d-flex">
|
||||
<div class="left-column">
|
||||
<img class="widgetLogo" src="<?php echo base_url(); ?>assets/logo/<?php echo $this->optionslib->get_logo('header_logo', $theme); ?>.png" alt="Logo" />
|
||||
</div>
|
||||
<?php if (!isset($error)) { ?>
|
||||
<div class="right-column">
|
||||
<div class="top-right d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="status-indicator <?php echo $is_on_air ? 'on-air' : 'off-air'; ?>"></span>
|
||||
<span class="<?= $text_size_class ?>" id="status-text">
|
||||
<?php
|
||||
if ($is_on_air === true) {
|
||||
printf("%s is ON-AIR", $user_callsign);
|
||||
} else {
|
||||
printf("%s is OFF-AIR", $user_callsign);
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="refresh-indicator" id="refresh-indicator">
|
||||
<small>🔄</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-right mt-3" id="frequency-container">
|
||||
<?php if ($is_on_air === true) { ?>
|
||||
<?php foreach ($radios_online as $radio_data) { ?>
|
||||
<div class="frequency-info mb-2">
|
||||
<span class="frequency-display <?= $text_size_class ?>" id="frequency-display">
|
||||
<?= htmlspecialchars($radio_data->frequency_string); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php } // end foreach ?>
|
||||
<?php } else if ($last_seen_text !== null) { ?>
|
||||
<p class="<?= $text_size_class ?>">
|
||||
<?= sprintf($last_seen_text); ?>
|
||||
<p class="<?= $text_size_class ?>" id="last-seen-text">
|
||||
<?= htmlspecialchars($last_seen_text); ?>
|
||||
</p>
|
||||
<?php } ?>
|
||||
<div class="last-updated mt-2">
|
||||
<small id="last-updated-text">Last updated: <?= date('H:i:s'); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
@@ -102,11 +243,105 @@ To use this widget insert this Element:
|
||||
<p class="<?= $text_size_class ?>"><?= __("Error") ?></p>
|
||||
</div>
|
||||
<div class="bottom-right mt-3">
|
||||
<p class="<?= $text_size_class ?>"><?= $error ?></p>
|
||||
<p class="<?= $text_size_class ?>"><?= htmlspecialchars($error) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!isset($nojs) || !$nojs): ?>
|
||||
<script>
|
||||
(function() {
|
||||
const userSlug = '<?php echo $user_slug ?? ''; ?>';
|
||||
let updateInterval;
|
||||
|
||||
function updateWidget() {
|
||||
if (!userSlug) return;
|
||||
|
||||
const refreshIndicator = document.getElementById('refresh-indicator');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const statusIndicator = document.querySelector('.status-indicator');
|
||||
const frequencyContainer = document.getElementById('frequency-container');
|
||||
const lastUpdatedText = document.getElementById('last-updated-text');
|
||||
|
||||
// Show loading state
|
||||
if (refreshIndicator) {
|
||||
refreshIndicator.classList.add('updating');
|
||||
}
|
||||
|
||||
fetch(`${window.location.origin}/index.php/widgets/on_air_ajax/${userSlug}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('Widget error:', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = `${data.user_callsign} is ${data.is_on_air ? 'ON-AIR' : 'OFF-AIR'}`;
|
||||
}
|
||||
|
||||
if (statusIndicator) {
|
||||
statusIndicator.className = `status-indicator ${data.is_on_air ? 'on-air' : 'off-air'}`;
|
||||
}
|
||||
|
||||
if (frequencyContainer && data.radios_online) {
|
||||
let html = '';
|
||||
|
||||
if (data.radios_online.length > 0) {
|
||||
data.radios_online.forEach(radio => {
|
||||
html += `
|
||||
<div class="frequency-info mb-2">
|
||||
<span class="frequency-display ${window.textSizeClass || ''}">
|
||||
${radio.frequency_string}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '<div class="last-updated mt-2"><small id="last-updated-text">Last updated: ' +
|
||||
new Date().toLocaleTimeString() + '</small></div>';
|
||||
} else if (data.last_seen_text) {
|
||||
html = `<p class="${window.textSizeClass || ''}" id="last-seen-text">${data.last_seen_text}</p>`;
|
||||
html += '<div class="last-updated mt-2"><small id="last-updated-text">Last updated: ' +
|
||||
new Date().toLocaleTimeString() + '</small></div>';
|
||||
} else {
|
||||
html = '<div class="last-updated mt-2"><small id="last-updated-text">Last updated: ' +
|
||||
new Date().toLocaleTimeString() + '</small></div>';
|
||||
}
|
||||
|
||||
frequencyContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
if (lastUpdatedText) {
|
||||
lastUpdatedText.textContent = 'Last updated: ' + new Date().toLocaleTimeString();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Widget update error:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (refreshIndicator) {
|
||||
refreshIndicator.classList.remove('updating');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.textSizeClass = '<?php echo $text_size_class ?? ''; ?>';
|
||||
|
||||
setTimeout(() => {
|
||||
updateWidget();
|
||||
updateInterval = setInterval(updateWidget, 30000);
|
||||
}, 2000);
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user