Files
spothole/webassets/js/common.js
2026-01-30 20:56:18 +00:00

192 lines
8.9 KiB
JavaScript

// Storage for the options that the server gives us. This will define our filters.
var options = {};
// Last time we updated the spots/alerts list on display.
var lastUpdateTime;
// Load and apply any URL params. This is used for "embedded mode" where another site can embed a version of
// Spothole and provide its own interface options rather than using the user's saved ones. These may select things
// from the various filter & display options, so this 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 occurs..
function loadURLParams() {
let params = new URLSearchParams(document.location.search);
// Handle embedded mode. We set a global to e.g. suppress loading/saving settings, and apply an attribute to the
// top-level html element to use CSS selectors to remove bits of UI.
let embedded = params.get("embedded");
if (embedded != null && embedded === "true") {
useLocalStorage = false;
$("html").attr("embedded-mode", "true");
}
// Handle other params
updateSelectFromParam(params, "color-scheme", "color-scheme");
updateSelectFromParam(params, "time-zone", "time-zone"); // Only on Spots and Alerts pages
updateSelectFromParam(params, "limit", "spots-to-fetch"); // Only on Spots page
updateSelectFromParam(params, "limit", "alerts-to-fetch"); // Only on Alerts page
updateSelectFromParam(params, "max_age", "max-spot-age"); // Only on Map & Bands pages
updateFilterFromParam(params, "band", "band");
updateFilterFromParam(params, "sig", "sig");
updateFilterFromParam(params, "source", "source");
updateFilterFromParam(params, "mode", "mode");
updateFilterFromParam(params, "dx_continent", "dx_continent");
updateFilterFromParam(params, "de_continent", "de_continent");
}
// Update an HTML checkbox element so that its selected matches the given parameter (which must have a true or false value)
function updateCheckboxFromParam(params, paramName, checkboxID) {
let v = params.get(paramName);
if (v != null) {
$("#" + checkboxID).prop("checked", (v === "true") ? true : false);
}
}
// Update an HTML select element so that its value matches the given parameter
function updateSelectFromParam(params, paramName, selectID) {
let v = params.get(paramName);
if (v != null) {
$("#" + selectID).prop("value", v);
// Extra check if this is the "color scheme" select
if (selectID == "color-scheme") {
setColorScheme(v);
}
}
}
// Update a set of HTML checkbox elements describing a filter of the given name, so that any items named in the
// parameter (as a comma-separated list) will be enabled, and all others disabled. e.g. if paramName is
// "filter-band" and the params contain "filter-band=20m,40m", and prefix is "band", then #filter-button-band-30m
// would be disabled but #filter-button-band-20m and #filter-button-band-40m would be enabled.
function updateFilterFromParam(params, paramName, filterName) {
let v = params.get(paramName);
if (v != null) {
// First uncheck all options for the filter
$(".filter-button-" + filterName).prop("checked", false);
// Now find out which ones should be enabled
let s = v.split(",");
s.forEach(val => $("#filter-button-" + filterName + "-" + val).prop("checked", true));
}
}
// For a parameter, such as dx_continent, get the query string for the current filter options.
function getQueryStringFor(parameter) {
return parameter + "=" + encodeURIComponent(getSelectedFilterOptions(parameter));
}
// For a parameter, such as dx_continent, get the filter options that are currently selected in the UI.
function getSelectedFilterOptions(parameter) {
return $(".filter-button-" + parameter).filter(function() {
return this.checked;
}).map(function() {
return this.value;
}).get().join(",");
}
// For a parameter, such as dx_continent, return true if all possible options are enabled. (In this case, we don't need
// to bother sending this as one of the query parameters to the API; no parameter provided implies "send everything".)
function allFilterOptionsSelected(parameter) {
var filter = $(".filter-button-" + parameter).filter(function() {
return !this.checked;
}).get();
return filter.length == 0;
}
// Generate a filter card with multiple toggle buttons plus All/None buttons.
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
// Create a button for each option
options.forEach(o => {
$(elementID).append(`<input type="checkbox" class="btn-check filter-button-${filterQuery} storeable-checkbox" name="options" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" for="filter-button-${filterQuery}-${o}">${o}</label> `);
});
// Create All/None buttons
$(elementID).append(` <span style="display: inline-block"><button id="filter-button-${filterQuery}-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', true);">All</button>&nbsp;<button id="filter-button-${filterQuery}-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', false);">None</button></span>`);
}
// Method called when "All" or "None" is clicked
function toggleFilterButtons(filterQuery, state) {
$(".filter-button-" + filterQuery).each(function() {
$(this).prop('checked', state);
});
filtersUpdated();
}
// Update the refresh timing display
function updateRefreshDisplay() {
if (lastUpdateTime != null) {
let secSinceUpdate = moment.duration(moment().diff(lastUpdateTime)).asSeconds();
let count = REFRESH_INTERVAL_SEC;
let updatingString = "Updating..."
if (secSinceUpdate < REFRESH_INTERVAL_SEC) {
count = REFRESH_INTERVAL_SEC - secSinceUpdate;
if (count <= 60) {
var number = count.toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " second" + (number != "1" ? "s" : "") + ".</span>";
} else {
var number = Math.round(count / 60.0).toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " minute" + (number != "1" ? "s" : "") + ".</span>";
}
}
$("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString);
}
}
// When the "use local time" field is changed, reload the table and save settings
function timeZoneUpdated() {
updateTable();
saveSettings();
}
// When one of the column toggle checkboxes are changed, reload the table and save settings
function columnsUpdated() {
updateTable();
saveSettings();
}
// Function to set the colour scheme based on the state of the UI select box
function setColorSchemeFromUI() {
setColorScheme($("#color-scheme option:selected").val());
saveSettings();
}
// Function to set the color scheme. Supported values: "dark", "light", "auto"
function setColorScheme(mode) {
let effectiveModeDark = mode == "dark";
if (mode == "auto") {
effectiveModeDark = window.matchMedia('(prefers-color-scheme: dark)').matches
}
$("html").attr("data-bs-theme", effectiveModeDark ? "dark" : "light");
const metaThemeColor = document.querySelector("meta[name=theme-color]");
metaThemeColor.setAttribute("content", effectiveModeDark ? "black" : "white");
const metaAppleStatusBarStyle = document.querySelector("meta[name=apple-mobile-web-app-status-bar-style]");
metaAppleStatusBarStyle.setAttribute("content", effectiveModeDark ? "black-translucent" : "white-translucent");
}
// Startup function to determine whether to use light or dark mode, or leave as auto
function usePreferredTheme() {
// Set the value of the select box to the server's default
$("#color-scheme").val(web_ui_options["default-color-scheme"]);
// 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 retain its default value from Spothole config and apply that.
let val = localStorage.getItem("#color-scheme:value");
if (val != null) {
setColorScheme(JSON.parse(val));
} else {
setColorSchemeFromUI();
}
}
// Sets up a listener on the OS light-dark theme change. If the Spothole user theme is set to Auto, the UI will be
// updated, otherwise if the Spothole user theme is forced to light or dark, that preference will remain.
function listenForOSThemeChange() {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
setColorScheme($("#color-scheme option:selected").val());
});
}
// Startup
$(document).ready(function() {
usePreferredTheme();
listenForOSThemeChange();
});