mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-28 18:22:05 +00:00
Replace pages' use of the options call with directly building the HTML components using templates, as the syntax is much nicer. Use a common set of data provided to all modules via pagetemplate.py instead of passing things like web_ui_options around. Closes #153
This commit is contained in:
+14
-47
@@ -26,45 +26,8 @@ const PROVIDER_CREDENTIAL_SCHEMAS = {
|
||||
]
|
||||
};
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
|
||||
// any saved values from local storage.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v3/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// Populate modes drop-down
|
||||
$.each(options["modes"], function (i, m) {
|
||||
$('#mode').append($('<option>', {
|
||||
value: m,
|
||||
text: m
|
||||
}));
|
||||
});
|
||||
|
||||
// Populate activity drop-down
|
||||
$.each(options["activities"], function (i, activity) {
|
||||
$('#activity').append($('<option>', {
|
||||
value: activity.name,
|
||||
text: activity.name
|
||||
}));
|
||||
});
|
||||
|
||||
// Load reCAPTCHA if a site key is configured (key is inlined into page by server)
|
||||
if (window._recaptchaSiteKey) {
|
||||
loadRecaptcha(window._recaptchaSiteKey);
|
||||
}
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// Update the upstream area for any pre-selected activity
|
||||
updateUpstreamArea();
|
||||
});
|
||||
}
|
||||
|
||||
// Load and inject the reCAPTCHA script
|
||||
function loadRecaptcha(siteKey) {
|
||||
window._recaptchaSiteKey = siteKey;
|
||||
function loadRecaptcha() {
|
||||
if (!document.getElementById('recaptcha-script')) {
|
||||
const script = document.createElement('script');
|
||||
script.id = 'recaptcha-script';
|
||||
@@ -79,20 +42,19 @@ function loadRecaptcha(siteKey) {
|
||||
// Called by reCAPTCHA after its script loads
|
||||
function renderRecaptcha() {
|
||||
window._recaptchaWidgetId = grecaptcha.render('recaptcha-widget', {
|
||||
sitekey: window._recaptchaSiteKey,
|
||||
sitekey: RECAPTCHA_SITE_KEY,
|
||||
size: 'normal'
|
||||
});
|
||||
}
|
||||
|
||||
// Update the "Send spot to..." area based on the currently selected activity
|
||||
function updateUpstreamArea() {
|
||||
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
|
||||
if (!ALLOW_UPSTREAM_SPOTTING) {
|
||||
$("#upstream-area").hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = $("#activity").val();
|
||||
const providers = (activity && options["spot_submit_providers"][activity]) ? options["spot_submit_providers"][activity] : [];
|
||||
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
|
||||
|
||||
if (providers.length === 0) {
|
||||
$("#upstream-area").hide();
|
||||
@@ -131,9 +93,7 @@ function updateCredentialsButton() {
|
||||
|
||||
// Get the currently selected upstream provider name
|
||||
function getSelectedUpstreamProvider() {
|
||||
const providers = (options && options["spot_submit_providers"] && $("#activity").val())
|
||||
? (options["spot_submit_providers"][$("#activity").val()] || [])
|
||||
: [];
|
||||
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
|
||||
if (providers.length === 0) return null;
|
||||
if (providers.length === 1) return providers[0];
|
||||
return $("#upstream-provider-select").val();
|
||||
@@ -302,6 +262,13 @@ $("#upstream-provider-select").change(function () {
|
||||
|
||||
// Startup
|
||||
$(document).ready(function () {
|
||||
// Load options
|
||||
loadOptions();
|
||||
// Load reCAPTCHA if a site key is configured
|
||||
if (RECAPTCHA_SITE_KEY) {
|
||||
loadRecaptcha();
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
|
||||
// Update the upstream area for any pre-selected activity
|
||||
updateUpstreamArea();
|
||||
});
|
||||
|
||||
+9
-29
@@ -328,33 +328,6 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
});
|
||||
}
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we then query alerts.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v3/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// Populate the filters panel
|
||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||
generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]);
|
||||
// Alerts can only ever be tagged with activities that have an alert source, so only offer those as filters here
|
||||
generateActivitiesMultiToggleFilterCard(options["activities"].filter(o => o["alerts_possible"]), false);
|
||||
|
||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||
// loading settings, so this needs to be called before that.
|
||||
loadURLParams();
|
||||
|
||||
// Load filters from settings storage
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
|
||||
// Load alerts and set up the timer
|
||||
loadAlerts();
|
||||
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Method called when any filter is changed to reload the alerts and persist the filter settings.
|
||||
function filtersUpdated() {
|
||||
loadAlerts();
|
||||
@@ -363,8 +336,15 @@ function filtersUpdated() {
|
||||
|
||||
// Startup
|
||||
$(document).ready(function () {
|
||||
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
|
||||
loadOptions();
|
||||
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
|
||||
// called first.
|
||||
loadURLParams();
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
|
||||
// Load alerts and set up the timer
|
||||
loadAlerts();
|
||||
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
|
||||
});
|
||||
|
||||
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
|
||||
|
||||
+14
-42
@@ -108,7 +108,7 @@ function updateBands() {
|
||||
// Convert to a map of band names to the spots on that band. Bands with no
|
||||
// spots in view will not be present.
|
||||
const bandToSpots = new Map();
|
||||
options["bands"].forEach(function (band) {
|
||||
BANDS.forEach(function (band) {
|
||||
const matchingSpots = spotList.filter(function (s) {
|
||||
return s.band === band.name;
|
||||
});
|
||||
@@ -127,7 +127,7 @@ function updateBands() {
|
||||
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
|
||||
|
||||
// Get the band data to fetch start and end frequencies
|
||||
let band = options["bands"].filter(function (b) {
|
||||
let band = BANDS.filter(function (b) {
|
||||
return b.name === bandName;
|
||||
})[0];
|
||||
|
||||
@@ -278,44 +278,6 @@ function removeDuplicatesForBandPanel(spotList) {
|
||||
return spotList.filter(s => !spotsToRemove.includes(s.uid));
|
||||
}
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||
// spots repeatedly.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v3/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
|
||||
// Add CSS for band toggle buttons
|
||||
addBandToggleColourCSS(options["bands"]);
|
||||
|
||||
// Populate the filters panel
|
||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||
generateModesMultiToggleFilterCard(options["modes"]);
|
||||
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
|
||||
|
||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||
// loading settings, so this needs to be called before that.
|
||||
loadURLParams();
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// Load spots and start SSE for live updates
|
||||
loadSpots();
|
||||
// Set up spot expiry checker
|
||||
setInterval(expireOldSpots, 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Method called when any display property is changed to reload the bands display and persist settings.
|
||||
function displayUpdated() {
|
||||
updateBands();
|
||||
@@ -331,6 +293,16 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
|
||||
// Call loadOptions(), this will then trigger loading spots and setting up timers.
|
||||
loadOptions();
|
||||
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
|
||||
// called first.
|
||||
loadURLParams();
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
addBandToggleColourCSS();
|
||||
|
||||
// Load spots and start SSE for live updates
|
||||
loadSpots();
|
||||
// Set up spot expiry checker
|
||||
setInterval(expireOldSpots, 60 * 1000);
|
||||
});
|
||||
+1
-73
@@ -1,5 +1,3 @@
|
||||
// Storage for the options that the server gives us. This will define our filters.
|
||||
let options = {};
|
||||
// Normally load user settings from local storage, unless embedded mode is in use
|
||||
let useLocalStorage = true;
|
||||
|
||||
@@ -124,75 +122,6 @@ function allFilterOptionsSelected(parameter) {
|
||||
return filter.length === 0;
|
||||
}
|
||||
|
||||
|
||||
// Generate a filter card with inline checkboxes plus All/None links.
|
||||
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
|
||||
const $row = $('<div>');
|
||||
options.forEach(o => {
|
||||
$row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`);
|
||||
});
|
||||
$(elementID).append($row);
|
||||
$(elementID).append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('${filterQuery}', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('${filterQuery}', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Generate a collapsable section for the activities filter card, and append it to the given list. The section is
|
||||
// expanded by default if isExpanded is true. If hasContent is false (i.e. there are no activities to show in this
|
||||
// section), the section is omitted entirely rather than showing an empty collapsible heading.
|
||||
function appendActivityFilterSection($list, sectionId, heading, isExapanded, hasContent, buildBody) {
|
||||
if (!hasContent) {
|
||||
return;
|
||||
}
|
||||
const collapseId = `activity-options-${sectionId}-collapse`;
|
||||
const $li = $('<li class="mb-1">');
|
||||
$li.append(`<button class="btn-toggle${isExapanded ? '' : ' collapsed'}" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="${isExapanded ? 'true' : 'false'}" aria-controls="${collapseId}">${heading}</button>`);
|
||||
const $body = $(`<div class="collapse filter-section-body${isExapanded ? ' show' : ''}" id="${collapseId}">`);
|
||||
buildBody($body);
|
||||
$li.append($body);
|
||||
$list.append($li);
|
||||
}
|
||||
|
||||
// Build the grid of activity checkboxes for a given (already filtered) list of activities.
|
||||
function buildActivityFilterGrid(activity_options) {
|
||||
const $grid = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||
activity_options.forEach(o => {
|
||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-${domSafeName}" for="filter-button-activity-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
||||
});
|
||||
return $grid;
|
||||
}
|
||||
|
||||
// Generate activities filter card. This one is also a special case. includeGeneralDX controls whether the "General
|
||||
// DX" (NO_ACTIVITY) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
|
||||
function generateActivitiesMultiToggleFilterCard(activity_options, includeGeneralDX = true) {
|
||||
const $list = $('<ul class="list-unstyled filter-section-list ps-0">');
|
||||
|
||||
const traditional = activity_options.filter(o => o["activity_type"] === "TRADITIONAL");
|
||||
appendActivityFilterSection($list, 'traditional', 'Traditional', true, includeGeneralDX || traditional.length > 0, $body => {
|
||||
if (includeGeneralDX) {
|
||||
$body.append(`<div class="w-100 mb-1"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-NO_ACTIVITY" for="filter-button-activity-NO_ACTIVITY"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
|
||||
}
|
||||
$body.append(buildActivityFilterGrid(traditional));
|
||||
});
|
||||
|
||||
const adventure = activity_options.filter(o => o["activity_type"] === "ADVENTURE");
|
||||
appendActivityFilterSection($list, 'adventure', 'Adventure', true, adventure.length > 0, $body => {
|
||||
$body.append(buildActivityFilterGrid(adventure));
|
||||
});
|
||||
|
||||
const regional = activity_options.filter(o => o["activity_type"] === "REGIONAL");
|
||||
appendActivityFilterSection($list, 'regional', 'Regional', false, regional.length > 0, $body => {
|
||||
$body.append(buildActivityFilterGrid(regional));
|
||||
});
|
||||
|
||||
const event = activity_options.filter(o => o["activity_type"] === "EVENT");
|
||||
appendActivityFilterSection($list, 'event', 'Event', false, event.length > 0, $body => {
|
||||
$body.append(buildActivityFilterGrid(event));
|
||||
});
|
||||
|
||||
$("#activity-options").append($list);
|
||||
$("#activity-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Method called when "All" or "None" is clicked
|
||||
function toggleFilterButtons(filterQuery, state) {
|
||||
$(".filter-button-" + filterQuery).each(function () {
|
||||
@@ -238,8 +167,7 @@ function setColorScheme(mode) {
|
||||
// Startup function to determine whether to use light or dark mode, or leave as auto
|
||||
function usePreferredTheme() {
|
||||
// Work out if we have ever explicitly saved the value of our select box. If so, we set our colour scheme now based
|
||||
// on that. If not, we let the select stay with nothing selected, so that the server sets it to whatever the
|
||||
// server's default is when the options call is retrieved.
|
||||
// on that. If not, we leave the colour scheme at the server's default, which is set in the page template.
|
||||
let val = localStorage.getItem("#color_scheme:value");
|
||||
if (val != null) {
|
||||
setColorScheme(JSON.parse(val));
|
||||
|
||||
+30
-57
@@ -322,61 +322,6 @@ function getTooltipText(s) {
|
||||
return ttt;
|
||||
}
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||
// spots repeatedly.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v3/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
|
||||
// Add CSS for band toggle buttons
|
||||
addBandToggleColourCSS(options["bands"]);
|
||||
|
||||
// Populate the filters panel
|
||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||
generateModesMultiToggleFilterCard(options["modes"]);
|
||||
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
|
||||
|
||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||
// loading settings, so this needs to be called before that.
|
||||
loadURLParams();
|
||||
loadMapURLParams();
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
|
||||
if (localStorage.getItem("#basemap:value") === null) {
|
||||
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
|
||||
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply basemap and overlay settings now that controls have their saved values
|
||||
setBasemap($("#basemap").val());
|
||||
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
|
||||
enableTerminator($("#showTerminator")[0].checked);
|
||||
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
|
||||
enableCQZones($("#showCQZones")[0].checked);
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
|
||||
// Load spots and start SSE for live updates
|
||||
loadSpots();
|
||||
// Set up spot expiry checker
|
||||
setInterval(expireOldSpots, 60 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Method called when any display property is changed to reload the map and persist the display settings.
|
||||
function displayUpdated() {
|
||||
updateMap();
|
||||
@@ -639,8 +584,36 @@ $(document).ready(function () {
|
||||
$(".hideonmap").hide();
|
||||
// Set up map
|
||||
setUpMap();
|
||||
// Call loadOptions(), this will then trigger loading spots and setting up timers.
|
||||
loadOptions();
|
||||
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
|
||||
// called first.
|
||||
loadURLParams();
|
||||
loadMapURLParams();
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
addBandToggleColourCSS();
|
||||
|
||||
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
|
||||
if (localStorage.getItem("#basemap:value") === null) {
|
||||
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
|
||||
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply basemap and overlay settings now that controls have their saved values
|
||||
setBasemap($("#basemap").val());
|
||||
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
|
||||
enableTerminator($("#showTerminator")[0].checked);
|
||||
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
|
||||
enableCQZones($("#showCQZones")[0].checked);
|
||||
enableITUZones($("#showITUZones")[0].checked);
|
||||
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
|
||||
|
||||
// Load spots and start SSE for live updates
|
||||
loadSpots();
|
||||
// Set up spot expiry checker
|
||||
setInterval(expireOldSpots, 60 * 1000);
|
||||
|
||||
// Display intro box
|
||||
displayIntroBox();
|
||||
// Prevent mouse scroll and touch actions in the popup menus being passed through to the map
|
||||
|
||||
+21
-48
@@ -461,52 +461,6 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
||||
return [$tr, $tr2];
|
||||
}
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||
// spots repeatedly.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v3/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
|
||||
// Add CSS for band toggle buttons
|
||||
addBandToggleColourCSS(options["bands"]);
|
||||
|
||||
// Populate the filters panel
|
||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||
generateModesMultiToggleFilterCard(options["modes"]);
|
||||
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
|
||||
|
||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||
// loading settings, so this needs to be called before that.
|
||||
loadURLParams();
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
|
||||
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
|
||||
// a value direct into the field, so apply the same logic here.
|
||||
$("#tableShowBearing").prop('disabled', !isUserGridValid());
|
||||
$("#tableShowDistance").prop('disabled', !isUserGridValid());
|
||||
if (!isUserGridValid()) {
|
||||
$("#tableShowBearing").prop('checked', false);
|
||||
$("#tableShowDistance").prop('checked', false);
|
||||
}
|
||||
|
||||
// Load spots (this will also set up the SSE connection to update them too)
|
||||
loadSpots();
|
||||
});
|
||||
}
|
||||
|
||||
// Work out if the user's entered grid is a valid Maidenhead grid
|
||||
function isUserGridValid() {
|
||||
const userGrid = $("#userGrid").val().toUpperCase();
|
||||
@@ -572,8 +526,27 @@ function clearWorked() {
|
||||
|
||||
// Startup
|
||||
$(document).ready(function () {
|
||||
// Call loadOptions(), this will then trigger loading spots and setting up timers.
|
||||
loadOptions();
|
||||
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
|
||||
// called first.
|
||||
loadURLParams();
|
||||
loadSettings();
|
||||
setColorScheme($("#color_scheme option:selected").val());
|
||||
setBandColorScheme($("#band_color_scheme option:selected").val());
|
||||
addBandToggleColourCSS();
|
||||
|
||||
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
|
||||
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
|
||||
// a value direct into the field, so apply the same logic here.
|
||||
$("#tableShowBearing").prop('disabled', !isUserGridValid());
|
||||
$("#tableShowDistance").prop('disabled', !isUserGridValid());
|
||||
if (!isUserGridValid()) {
|
||||
$("#tableShowBearing").prop('checked', false);
|
||||
$("#tableShowDistance").prop('checked', false);
|
||||
}
|
||||
|
||||
// Load spots (this will also set up the SSE connection to update them too)
|
||||
loadSpots();
|
||||
|
||||
// Display intro box
|
||||
displayIntroBox();
|
||||
|
||||
|
||||
@@ -7,80 +7,38 @@ let spots = [];
|
||||
let worked = []
|
||||
|
||||
// Dynamically add CSS code for the band checkboxes to show in the appropriate colour.
|
||||
// Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
|
||||
function addBandToggleColourCSS(band_options) {
|
||||
function addBandToggleColourCSS() {
|
||||
const $style = $('<style>');
|
||||
band_options.forEach(o => {
|
||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`);
|
||||
$(".filter-button-band").each(function () {
|
||||
$style.append(`label[for="${this.id}"] { padding-left: 0.3em; border-left: 5px solid ${bandToColor(this.value)};}`);
|
||||
});
|
||||
$('html > head').append($style);
|
||||
}
|
||||
|
||||
// Generate bands filter card. This one is a special case.
|
||||
function generateBandsMultiToggleFilterCard(band_options) {
|
||||
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">');
|
||||
band_options.forEach(o => {
|
||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`);
|
||||
});
|
||||
$("#band-options").append($grid);
|
||||
$("#band-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a> <a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a></div>`);
|
||||
}
|
||||
|
||||
// Set the band toggles so that only the amateur radio HF bands are selected. This includes 160m and 6m because that's
|
||||
// widely expected by hams to be included. Special case of toggleFilterButtons().
|
||||
// Set the band toggles so that only the amateur radio HF bands are selected. Special case of toggleFilterButtons().
|
||||
function setHamHFBandToggles() {
|
||||
const hamHFBands = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"];
|
||||
$(".filter-button-band").each(function () {
|
||||
$(this).prop('checked', hamHFBands.includes($(this).val().replace("filter-button-band-", "")));
|
||||
$(this).prop('checked', $(this).data("ham-hf"));
|
||||
});
|
||||
filtersUpdated();
|
||||
}
|
||||
|
||||
// Generate modes filter card. This one is also a special case.
|
||||
function generateModesMultiToggleFilterCard(mode_options) {
|
||||
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">');
|
||||
mode_options.forEach(o => {
|
||||
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`);
|
||||
});
|
||||
$("#mode-options").append($grid);
|
||||
$("#mode-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a> <a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a> <a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a></div>`);
|
||||
}
|
||||
|
||||
// Set the mode toggles that relate to Analog Voice.
|
||||
function setVoiceModeToggles() {
|
||||
const modes = ["PHONE", "SSB", "AM", "FM", "DV"];
|
||||
$(".filter-button-mode").each(function () {
|
||||
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
|
||||
$(this).prop('checked', $(this).data("mode-type") === "PHONE");
|
||||
});
|
||||
filtersUpdated();
|
||||
}
|
||||
|
||||
// Set the mode toggles that relate to Digimodes.
|
||||
function setDigiModeToggles() {
|
||||
const modes = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "PSK", "OLIVIA", "PKT", "MSK144"];
|
||||
$(".filter-button-mode").each(function () {
|
||||
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
|
||||
$(this).prop('checked', $(this).data("mode-type") === "DATA");
|
||||
});
|
||||
filtersUpdated();
|
||||
}
|
||||
|
||||
// Generate Sources filter card. This one is a minor special case as we create the checkboxes in the normal way, but
|
||||
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
|
||||
// names here for HTML elements.
|
||||
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
|
||||
const $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
|
||||
source_options.forEach(o => {
|
||||
const enable = sources_enabled_by_default.includes(o);
|
||||
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`);
|
||||
});
|
||||
$("#source-options").append($grid);
|
||||
$("#source-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a></div>`);
|
||||
}
|
||||
|
||||
// Method called when any filter is changed to reload the spots and persist the filter settings.
|
||||
function filtersUpdated() {
|
||||
loadSpots();
|
||||
|
||||
+60
-1
@@ -38,12 +38,18 @@
|
||||
<label for="mode" class="form-label">Mode</label>
|
||||
<select id="mode" class="form-select">
|
||||
<option value="" selected></option>
|
||||
{% for m in options["modes"] %}
|
||||
<option value="{{ m }}">{{ m }}</option>
|
||||
{% end %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label for="activity" class="form-label">Activity</label>
|
||||
<select id="activity" class="form-select">
|
||||
<option value="" selected></option>
|
||||
{% for a in options["activities"] %}
|
||||
<option value="{{ a.name }}">{{ a.name }}</option>
|
||||
{% end %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@@ -68,6 +74,32 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="upstream-area" class="mt-3" style="display:none;">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="submit-upstream">
|
||||
<label class="form-check-label" for="submit-upstream">
|
||||
Send spot to <span id="upstream-provider-label"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" id="upstream-provider-select-col" style="display:none;">
|
||||
<select id="upstream-provider-select" class="form-select form-select-sm"></select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="upstream-credentials-btn"
|
||||
onclick="showCredentialsModal();" style="display:none;">
|
||||
<i class="fa-solid fa-key"></i> Credentials
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recaptcha-area" class="mt-3" style="display:none;">
|
||||
<div id="recaptcha-widget"></div>
|
||||
</div>
|
||||
|
||||
<div id="result-good"></div>
|
||||
<div id="result-bad"></div>
|
||||
|
||||
@@ -77,7 +109,34 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1790344336"></script>
|
||||
<div class="modal fade" id="credentials-modal" tabindex="-1" aria-labelledby="credentials-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="credentials-modal-label">Credentials for
|
||||
<span id="credentials-provider-name"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted">Credentials are stored only in your browser and are never saved on the
|
||||
server.</p>
|
||||
<div id="credentials-fields"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveCredentials();">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SPOT_SUBMIT_PROVIDERS = {% raw safe_json_dumps(options["spot_submit_providers"]) %};
|
||||
const RECAPTCHA_SITE_KEY = {% raw safe_json_dumps(web_ui_options["recaptcha_site_key"]) %};
|
||||
const ALLOW_UPSTREAM_SPOTTING = {% raw safe_json_dumps(web_ui_options["allow_upstream_spotting"]) %};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1790367752"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+18
-16
@@ -7,45 +7,47 @@
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-inline-flex gap-1">
|
||||
{% module Template("widgets/filters_display_data_buttons.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_display_data_buttons.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filters-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/dx_continent.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/duration_limit_alerts.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/duration_limit_alerts.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/activities.html",
|
||||
activities=[a for a in options["activities"] if a.alerts_possible], include_general_dx=False) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/sources.html",
|
||||
sources=options["alert_providers"], sources_enabled_by_default=options["alert_providers"]) %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="display-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/display_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/time_zone.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/time_zone.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/number_of_alerts.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/number_of_alerts.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/color_scheme.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/color_scheme.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/table_columns_alerts.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/table_columns_alerts.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,21 +55,21 @@
|
||||
|
||||
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
|
||||
<div id="data-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/your_data_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
{% if web_ui_options["qrz_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/qrz.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
{% if web_ui_options["hamqth_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/hamqth.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="col">
|
||||
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/wanted_calls.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1790344337"></script>
|
||||
<script src="/static/js/alerts.js?v=1790367751"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+24
-18
@@ -6,44 +6,46 @@
|
||||
<div class="col-auto me-auto pt-3"></div>
|
||||
<div class="col-auto">
|
||||
<div class="d-inline-flex gap-1">
|
||||
{% module Template("widgets/filters_display_data_buttons.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_display_data_buttons.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filters-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
|
||||
<div class="col">
|
||||
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/bands.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/dx_continent.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/de_continent.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/activities.html",
|
||||
activities=options["activities"], include_general_dx=True) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/sources.html",
|
||||
sources=options["spot_providers"],
|
||||
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/modes.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="display-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/display_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/spot_age.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/spot_age.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html", web_ui_options=web_ui_options)
|
||||
%}
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,21 +53,21 @@
|
||||
|
||||
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
|
||||
<div id="data-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/your_data_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
{% if web_ui_options["qrz_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/qrz.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
{% if web_ui_options["hamqth_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/hamqth.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="col">
|
||||
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/wanted_calls.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,8 +78,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790344336"></script>
|
||||
<script src="/static/js/bands.js?v=1790344336"></script>
|
||||
<script>
|
||||
const BANDS = {% raw safe_json_dumps(options["bands"]) %};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790367752"></script>
|
||||
<script src="/static/js/bands.js?v=1790367752"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790344336" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1790367751" type="text/css">
|
||||
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
|
||||
@@ -16,10 +16,10 @@
|
||||
window.fetchEventSource = fetchEventSource;
|
||||
</script>
|
||||
|
||||
<script src="/static/js/utils.js?v=1790344336"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790344336"></script>
|
||||
<script src="/static/js/geo.js?v=1790344336"></script>
|
||||
<script src="/static/js/common.js?v=1790344336"></script>
|
||||
<script src="/static/js/utils.js?v=1790367751"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1790367751"></script>
|
||||
<script src="/static/js/geo.js?v=1790367751"></script>
|
||||
<script src="/static/js/common.js?v=1790367751"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
@@ -43,7 +43,7 @@
|
||||
class="fa-solid fa-ruler-vertical"></i> Bands</a></li>
|
||||
<li class="nav-item ms-4"><a href="/alerts" class="nav-link" id="nav-link-alerts"><i
|
||||
class="fa-solid fa-clock"></i> Upcoming</a></li>
|
||||
{% if allow_spotting %}
|
||||
{% if options["spot_allowed"] %}
|
||||
<li class="nav-item ms-4"><a href="/add-spot" class="nav-link" id="nav-link-add-spot"><i
|
||||
class="fa-solid fa-comment"></i> Add Spot</a></li>
|
||||
{% end %}
|
||||
|
||||
@@ -1,6 +1,58 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Activities</h5>
|
||||
<div id="activity-options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<ul class="list-unstyled filter-section-list ps-0">
|
||||
{% for section_type, heading, expanded in [("TRADITIONAL", "Traditional", True), ("ADVENTURE",
|
||||
"Adventure", True), ("REGIONAL", "Regional", False), ("EVENT", "Event", False)] %}
|
||||
{% set show_general_dx = include_general_dx and section_type == "TRADITIONAL" %}
|
||||
{% set section_activities = [a for a in activities if a.activity_type == section_type] %}
|
||||
{% if section_activities or show_general_dx %}
|
||||
{% set collapse_id = "activity-options-" + section_type.lower() + "-collapse" %}
|
||||
<li class="mb-1">
|
||||
<button class="btn-toggle{% if not expanded %} collapsed{% end %}" data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapse_id }}" aria-expanded="{{ 'true' if expanded else 'false' }}"
|
||||
aria-controls="{{ collapse_id }}">{{ heading }}
|
||||
</button>
|
||||
<div class="collapse filter-section-body{% if expanded %} show{% end %}" id="{{ collapse_id }}">
|
||||
{% if show_general_dx %}
|
||||
<div class="w-100 mb-1">
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
class="form-check-input filter-button-activity storeable-checkbox"
|
||||
id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label" for="filter-button-activity-NO_ACTIVITY"><i
|
||||
class="fa-solid fa-tower-cell"></i> General DX</label>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="row row-cols-2 g-1 mb-1">
|
||||
{% for a in section_activities %}
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
class="form-check-input filter-button-activity storeable-checkbox"
|
||||
id="filter-button-activity-{{ dom_safe_name(a.name) }}" value="{{ a.name }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label"
|
||||
for="filter-button-activity-{{ dom_safe_name(a.name) }}"
|
||||
title="{{ a.description }}"><i class="fa-solid {{ a.icon }}"></i> {{ a.name
|
||||
}}
|
||||
{{ a.region_flag or "" }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% end %}
|
||||
{% end %}
|
||||
</ul>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Bands</h5>
|
||||
<div id="band-options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">
|
||||
{% for b in options["bands"] %}
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input filter-button-band storeable-checkbox"
|
||||
id="filter-button-band-{{ dom_safe_name(b.name) }}" value="{{ b.name }}"
|
||||
data-ham-hf="{{ 'true' if b.is_ham_hf else 'false' }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label" for="filter-button-band-{{ dom_safe_name(b.name) }}">{{ b.name
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a>
|
||||
<a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Theme</h5>
|
||||
<p class="card-text spothole-card-text">
|
||||
{% module Template("widgets/color_scheme.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/color_scheme.html") %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,10 +2,10 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Theme</h5>
|
||||
<p class="card-text spothole-card-text">
|
||||
{% module Template("widgets/color_scheme.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/color_scheme.html") %}
|
||||
</p>
|
||||
<p class="card-text spothole-card-text">
|
||||
{% module Template("widgets/band_color_scheme.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/band_color_scheme.html") %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,21 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DE Continent</h5>
|
||||
<div id="de_continent_options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<div>
|
||||
{% for c in options["continents"] %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input filter-button-de_continent storeable-checkbox"
|
||||
id="filter-button-de_continent-{{ c }}" value="{{ c }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label" for="filter-button-de_continent-{{ c }}">{{ c }}</label>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('de_continent', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('de_continent', false); return false;">None</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DX Continent</h5>
|
||||
<div id="dx_continent_options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<div>
|
||||
{% for c in options["continents"] %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="checkbox" class="form-check-input filter-button-dx_continent storeable-checkbox"
|
||||
id="filter-button-dx_continent-{{ c }}" value="{{ c }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label" for="filter-button-dx_continent-{{ c }}">{{ c }}</label>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('dx_continent', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('dx_continent', false); return false;">None</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Modes</h5>
|
||||
<div id="mode-options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">
|
||||
{% for m in options["modes"] %}
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox"
|
||||
id="filter-button-mode-{{ dom_safe_name(m) }}" value="{{ m }}"
|
||||
data-mode-type="{{ infer_mode_type_from_mode(m).value }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" checked>
|
||||
<label class="form-check-label" for="filter-button-mode-{{ dom_safe_name(m) }}">{{ m }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a>
|
||||
<a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a>
|
||||
<a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sources</h5>
|
||||
<div id="source-options" class="card-text spothole-card-text"></div>
|
||||
<div class="card-text spothole-card-text">
|
||||
<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">
|
||||
{% for s in sources %}
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input filter-button-source storeable-checkbox"
|
||||
id="filter-button-source-{{ dom_safe_name(s) }}" value="{{ s }}"
|
||||
autocomplete="off" onClick="filtersUpdated()" {% if s in sources_enabled_by_default
|
||||
%}checked{% end %}>
|
||||
<label class="form-check-label" for="filter-button-source-{{ dom_safe_name(s) }}">{{ s
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% end %}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a>
|
||||
<a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
||||
<script src="/static/js/conditions.js?v=1790344336"></script>
|
||||
<script src="/static/js/conditions.js?v=1790367751"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<li><strong><i class="fa-solid fa-clock"></i> Upcoming</strong>, the list of upcoming activations, DXpeditions and
|
||||
contests
|
||||
</li>
|
||||
{% if allow_spotting %}
|
||||
{% if options["spot_allowed"] %}
|
||||
<li><strong><i class="fa-solid fa-comment"></i> Add Spot</strong>, which allows users to add new spots to the system
|
||||
</li>
|
||||
{% end %}
|
||||
|
||||
+22
-20
@@ -20,50 +20,52 @@
|
||||
<div class="col-auto me-auto pt-3"></div>
|
||||
<div class="col-auto">
|
||||
<div class="d-inline-flex gap-1">
|
||||
{% module Template("widgets/filters_display_data_buttons.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_display_data_buttons.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filters-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
|
||||
<div class="col">
|
||||
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/bands.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/dx_continent.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/de_continent.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/activities.html",
|
||||
activities=options["activities"], include_general_dx=True) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/sources.html",
|
||||
sources=options["spot_providers"],
|
||||
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/modes.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="display-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/display_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/spot_age.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/spot_age.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/basemap.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/basemap.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/map_features.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/map_features.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html",
|
||||
web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,21 +73,21 @@
|
||||
|
||||
{% if web_ui_options["qrz_enabled"] or web_ui_options["hamqth_enabled"] %}
|
||||
<div id="data-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/your_data_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
{% if web_ui_options["qrz_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/qrz.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
{% if web_ui_options["hamqth_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/hamqth.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="col">
|
||||
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/wanted_calls.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,8 +115,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790344337"></script>
|
||||
<script src="/static/js/map.js?v=1790344337"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790367751"></script>
|
||||
<script src="/static/js/map.js?v=1790367751"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+27
-25
@@ -30,85 +30,87 @@
|
||||
<div id="settingsButtonRow" class="row mb-3">
|
||||
<div class="col-md-4 mb-3 mb-md-0">
|
||||
<div class="d-inline-flex gap-3">
|
||||
{% module Template("widgets/run_pause.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/run_pause.html") %}
|
||||
<div class="d-inline-flex">{% raw web_ui_options["support_button_html"] %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 text-end">
|
||||
<div class="d-inline-flex gap-3">
|
||||
{% module Template("widgets/search.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_display_data_buttons.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/search.html") %}
|
||||
{% module Template("widgets/filters_display_data_buttons.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filters-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/filters_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/filters_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 g-4 mb-4 row-cols-md-3">
|
||||
<div class="col">
|
||||
{% module Template("cards/bands.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/bands.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/dx_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/dx_continent.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/de_continent.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/de_continent.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/activities.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/activities.html",
|
||||
activities=options["activities"], include_general_dx=True) %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/sources.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/sources.html",
|
||||
sources=options["spot_providers"],
|
||||
sources_enabled_by_default=options["spot_providers_enabled_by_default"]) %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/modes.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/modes.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="display-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/display_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/display_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div id="display-container" class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
{% module Template("cards/time_zone.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/time_zone.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/audio.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/audio.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/number_of_spots.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/number_of_spots.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html", web_ui_options=web_ui_options)
|
||||
%}
|
||||
{% module Template("cards/color_scheme_and_band_color_scheme.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/table_columns_spots.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/table_columns_spots.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="data-area" class="appearing-panel card mb-3">
|
||||
{% module Template("widgets/your_data_area_header.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("widgets/your_data_area_header.html") %}
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
{% if web_ui_options["qrz_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/qrz.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/qrz.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
{% if web_ui_options["hamqth_enabled"] %}
|
||||
<div class="col">
|
||||
{% module Template("cards/hamqth.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/hamqth.html") %}
|
||||
</div>
|
||||
{% end %}
|
||||
<div class="col">
|
||||
{% module Template("cards/location.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/location.html") %}
|
||||
<div class="mt-4"></div>
|
||||
{% module Template("cards/worked_calls.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/worked_calls.html") %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% module Template("cards/wanted_calls.html", web_ui_options=web_ui_options) %}
|
||||
{% module Template("cards/wanted_calls.html") %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,8 +127,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790344336"></script>
|
||||
<script src="/static/js/spots.js?v=1790344336"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1790367751"></script>
|
||||
<script src="/static/js/spots.js?v=1790367751"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1790344336"></script>
|
||||
<script src="/static/js/status.js?v=1790367751"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -7,6 +8,7 @@ from tornado.web import Application
|
||||
|
||||
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
|
||||
from core.constants import BANDS, PROPAGATION_MODES
|
||||
from core.data_providers import DATA_PROVIDERS
|
||||
from core.enums import Continent, Mode, ModeType
|
||||
from core.utils import safe_json_dumps
|
||||
from data.activities import ACTIVITIES
|
||||
@@ -14,6 +16,54 @@ from data.activities import ACTIVITIES
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def build_options():
|
||||
"""Build the options data that is returned by the /options API and used to populate the web UI templates. This
|
||||
doesn't change at runtime, so it is built once on first use and then cached."""
|
||||
|
||||
# Build a map of activity name -> list of provider names that can submit spots for that activity
|
||||
spot_submit_providers = {}
|
||||
|
||||
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
|
||||
# for provider in DATA_PROVIDERS.spot_providers:
|
||||
# if not provider.enabled:
|
||||
# continue
|
||||
# for activity in ACTIVITIES.values():
|
||||
# if provider.can_submit_spot(activity.name):
|
||||
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
|
||||
|
||||
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
|
||||
# things that aren't even available.
|
||||
spot_providers = [p.name for p in DATA_PROVIDERS.spot_providers if p.enabled]
|
||||
alert_providers = [p.name for p in DATA_PROVIDERS.alert_providers if p.enabled]
|
||||
callsign_data_providers = [p.name for p in DATA_PROVIDERS.callsign_data_providers if p.enabled]
|
||||
spot_providers_enabled_by_default = [
|
||||
p.name for p in DATA_PROVIDERS.spot_providers if p.enabled and p.enabled_by_default_in_web_ui
|
||||
]
|
||||
|
||||
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
|
||||
# one of our providers.
|
||||
if ALLOW_SPOTTING:
|
||||
spot_providers.append("API")
|
||||
spot_providers_enabled_by_default.append("API")
|
||||
|
||||
return {
|
||||
"bands": BANDS,
|
||||
"modes": [m.value for m in Mode],
|
||||
"mode_types": [t.value for t in ModeType],
|
||||
"activities": list(ACTIVITIES.values()),
|
||||
"spot_providers": spot_providers,
|
||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||
"alert_providers": alert_providers,
|
||||
"callsign_data_providers": callsign_data_providers,
|
||||
"continents": [c.value for c in Continent],
|
||||
"propagation_modes": list(PROPAGATION_MODES.values()),
|
||||
"max_spot_age": MAX_SPOT_AGE,
|
||||
"spot_allowed": ALLOW_SPOTTING,
|
||||
"spot_submit_providers": spot_submit_providers,
|
||||
}
|
||||
|
||||
|
||||
class APIOptionsHandler(tornado.web.RequestHandler):
|
||||
"""API request handler for /api/v3/options"""
|
||||
|
||||
@@ -23,73 +73,13 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
||||
request: httputil.HTTPServerRequest,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self._status_data = None
|
||||
self._spot_providers = None
|
||||
super().__init__(application, request, **kwargs)
|
||||
|
||||
def initialize(self, status_data, spot_providers=None):
|
||||
self._status_data = status_data
|
||||
self._spot_providers = spot_providers or []
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
# Build a map of activity name -> list of provider names that can submit spots for that activity
|
||||
spot_submit_providers = {}
|
||||
|
||||
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
|
||||
# for provider in self._spot_providers:
|
||||
# if not provider.enabled:
|
||||
# continue
|
||||
# for activity in ACTIVITIES.values():
|
||||
# if provider.can_submit_spot(activity.name):
|
||||
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
|
||||
|
||||
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
|
||||
# things that aren't even available.
|
||||
spot_providers: list = [
|
||||
p["name"] for p in filter(lambda p: p["enabled"], self._status_data["spot_providers"])
|
||||
]
|
||||
alert_providers = [p["name"] for p in filter(lambda p: p["enabled"], self._status_data["alert_providers"])]
|
||||
callsign_data_providers = [
|
||||
p["name"]
|
||||
for p in filter(
|
||||
lambda p: p["enabled"],
|
||||
self._status_data["callsign_data_providers"],
|
||||
)
|
||||
]
|
||||
spot_providers_enabled_by_default = [
|
||||
p["name"]
|
||||
for p in filter(
|
||||
lambda p: p["enabled"] and p["enabled_by_default_in_web_ui"],
|
||||
self._status_data["spot_providers"],
|
||||
)
|
||||
]
|
||||
|
||||
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
|
||||
# one of our providers.
|
||||
if ALLOW_SPOTTING:
|
||||
spot_providers.append("API")
|
||||
spot_providers_enabled_by_default.append("API")
|
||||
|
||||
options = {
|
||||
"bands": BANDS,
|
||||
"modes": [m.value for m in Mode],
|
||||
"mode_types": [t.value for t in ModeType],
|
||||
"activities": list(ACTIVITIES.values()),
|
||||
"spot_providers": spot_providers,
|
||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||
"alert_providers": alert_providers,
|
||||
"callsign_data_providers": callsign_data_providers,
|
||||
"continents": [c.value for c in Continent],
|
||||
"propagation_modes": list(PROPAGATION_MODES.values()),
|
||||
"max_spot_age": MAX_SPOT_AGE,
|
||||
"spot_allowed": ALLOW_SPOTTING,
|
||||
"spot_submit_providers": spot_submit_providers,
|
||||
}
|
||||
|
||||
self.write(safe_json_dumps(options))
|
||||
self.write(safe_json_dumps(build_options()))
|
||||
self.set_status(200)
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Cache-Control", "max-age=3600")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import tornado
|
||||
@@ -5,7 +6,6 @@ from tornado import httputil
|
||||
from tornado.web import Application
|
||||
|
||||
from core.config import (
|
||||
ALLOW_SPOTTING,
|
||||
BASE_URL,
|
||||
SERVER_OWNER_CALLSIGN,
|
||||
TELNET_SERVER_ADDRESS,
|
||||
@@ -14,6 +14,14 @@ from core.config import (
|
||||
WEB_UI_OPTIONS,
|
||||
)
|
||||
from core.constants import SOFTWARE_VERSION
|
||||
from core.utils import infer_mode_type_from_mode, safe_json_dumps
|
||||
from webserver.handlers.api.options import build_options
|
||||
|
||||
|
||||
def dom_safe_name(name):
|
||||
"""Strip characters from a name that are not allowed in HTML element IDs"""
|
||||
|
||||
return re.sub(r"^[^A-Za-z0-9]+|\W+", "", name, flags=re.ASCII)
|
||||
|
||||
|
||||
class PageTemplateHandler(tornado.web.RequestHandler):
|
||||
@@ -31,17 +39,27 @@ class PageTemplateHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, template_name):
|
||||
self._template_name = template_name
|
||||
|
||||
def get(self):
|
||||
# Load named template, and provide variables used in templates
|
||||
self.render(
|
||||
f"{self._template_name}.html",
|
||||
def get_template_namespace(self):
|
||||
"""Provide variables and helper functions to all page templates"""
|
||||
|
||||
namespace = super().get_template_namespace()
|
||||
namespace.update(
|
||||
# Various options
|
||||
options=build_options(),
|
||||
web_ui_options=WEB_UI_OPTIONS,
|
||||
# Other single static parameters
|
||||
software_version=SOFTWARE_VERSION,
|
||||
server_owner_callsign=SERVER_OWNER_CALLSIGN,
|
||||
allow_spotting=ALLOW_SPOTTING,
|
||||
web_ui_options=WEB_UI_OPTIONS,
|
||||
baseurl=BASE_URL,
|
||||
telnet_server_enabled=TELNET_SERVER_ENABLED,
|
||||
telnet_server_address=TELNET_SERVER_ADDRESS,
|
||||
telnet_server_port=TELNET_SERVER_PORT,
|
||||
current_path=self.request.path,
|
||||
# Function references that templates need
|
||||
dom_safe_name=dom_safe_name,
|
||||
safe_json_dumps=safe_json_dumps,
|
||||
infer_mode_type_from_mode=infer_mode_type_from_mode,
|
||||
)
|
||||
return namespace
|
||||
|
||||
def get(self):
|
||||
self.render(f"{self._template_name}.html")
|
||||
|
||||
+3
-15
@@ -161,11 +161,7 @@ class WebServer:
|
||||
APIDxStatsHandler,
|
||||
{"spots": self._data_store.spots},
|
||||
),
|
||||
(
|
||||
r"/api/v3/options",
|
||||
APIOptionsHandler,
|
||||
{"status_data": self._data_store.status.get()},
|
||||
),
|
||||
(r"/api/v3/options", APIOptionsHandler),
|
||||
(
|
||||
r"/api/v3/status",
|
||||
APIStatusHandler,
|
||||
@@ -216,11 +212,7 @@ class WebServer:
|
||||
V2APIDxStatsHandler,
|
||||
{"spots": self._data_store.spots},
|
||||
),
|
||||
(
|
||||
r"/api/v2/options",
|
||||
V2APIOptionsHandler,
|
||||
{"status_data": self._data_store.status.get()},
|
||||
),
|
||||
(r"/api/v2/options", V2APIOptionsHandler),
|
||||
(
|
||||
r"/api/v2/status",
|
||||
V2APIStatusHandler,
|
||||
@@ -271,11 +263,7 @@ class WebServer:
|
||||
V1APIDxStatsHandler,
|
||||
{"spots": self._data_store.spots},
|
||||
),
|
||||
(
|
||||
r"/api/v1/options",
|
||||
V1APIOptionsHandler,
|
||||
{"status_data": self._data_store.status.get()},
|
||||
),
|
||||
(r"/api/v1/options", V1APIOptionsHandler),
|
||||
(
|
||||
r"/api/v1/status",
|
||||
V1APIStatusHandler,
|
||||
|
||||
Reference in New Issue
Block a user