// 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(` `); }); // Create All/None buttons $(elementID).append(`  `); } // 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 = "Updating in " + number + " second" + (number != "1" ? "s" : "") + "."; } else { var number = Math.round(count / 60.0).toFixed(0); updatingString = "Updating in " + number + " minute" + (number != "1" ? "s" : "") + "."; } } $("#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() { // 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 of "auto". let val = localStorage.getItem("#color-scheme:value"); if (val != null) { setColorScheme(JSON.parse(val)); } } // 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(); });