mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 00:59:29 -07:00
Merge branch 'main' into personal-weather
This commit is contained in:
@@ -4,11 +4,12 @@ import {
|
||||
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, isIOS,
|
||||
} from './modules/navigation.mjs';
|
||||
import { round2 } from './modules/utils/units.mjs';
|
||||
import { parseQueryString } from './modules/share.mjs';
|
||||
import { registerHiddenSetting } from './modules/share.mjs';
|
||||
import settings from './modules/settings.mjs';
|
||||
import AutoComplete from './modules/autocomplete.mjs';
|
||||
import { loadAllData } from './modules/utils/data-loader.mjs';
|
||||
import { debugFlag } from './modules/utils/debug.mjs';
|
||||
import { parseQueryString } from './modules/utils/setting.mjs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
@@ -106,17 +107,34 @@ const init = async () => {
|
||||
|
||||
// attempt to parse the url parameters
|
||||
const parsedParameters = parseQueryString();
|
||||
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
||||
const loadFromParsed = !!parsedParameters.latLon;
|
||||
|
||||
// Auto load the parsed parameters and fall back to the previous query
|
||||
const query = parsedParameters.latLonQuery ?? localStorage.getItem('latLonQuery');
|
||||
const latLon = parsedParameters.latLon ?? localStorage.getItem('latLon');
|
||||
const fromGPS = localStorage.getItem('latLonFromGPS') && !loadFromParsed;
|
||||
|
||||
if (query && latLon && !fromGPS) {
|
||||
if (parsedParameters.latLonQuery && !parsedParameters.latLon) {
|
||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||
txtAddress.value = query;
|
||||
loadData(JSON.parse(latLon));
|
||||
txtAddress.value = parsedParameters.latLonQuery;
|
||||
const geometry = await geocodeLatLonQuery(parsedParameters.latLonQuery);
|
||||
if (geometry) {
|
||||
doRedirectToGeometry(geometry);
|
||||
}
|
||||
} else if (latLon && !fromGPS) {
|
||||
// update in-page search box if using cached data, or parsed parameter
|
||||
if ((query && !loadFromParsed) || (parsedParameters.latLonQuery && loadFromParsed)) {
|
||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||
txtAddress.value = query;
|
||||
}
|
||||
// use lat-long lookup if that's all that was provided in the query string
|
||||
if (loadFromParsed && parsedParameters.latLon && !parsedParameters.latLonQuery) {
|
||||
const { lat, lon } = JSON.parse(latLon);
|
||||
getForecastFromLatLon(lat, lon, true);
|
||||
} else {
|
||||
// otherwise use pre-stored data
|
||||
loadData(JSON.parse(latLon));
|
||||
}
|
||||
}
|
||||
if (fromGPS) {
|
||||
btnGetGpsClick();
|
||||
@@ -160,6 +178,30 @@ const init = async () => {
|
||||
// swipe functionality
|
||||
document.querySelector('#container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
||||
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
|
||||
|
||||
// register hidden settings for search and location query
|
||||
registerHiddenSetting('latLonQuery', () => localStorage.getItem('latLonQuery'));
|
||||
registerHiddenSetting('latLon', () => localStorage.getItem('latLon'));
|
||||
};
|
||||
|
||||
const geocodeLatLonQuery = async (query) => {
|
||||
try {
|
||||
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
|
||||
data: {
|
||||
text: query,
|
||||
f: 'json',
|
||||
},
|
||||
});
|
||||
|
||||
const loc = data.locations?.[0];
|
||||
if (loc) {
|
||||
return loc.feature.geometry;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Geocoding failed:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const autocompleteOnSelect = async (suggestion) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from './utils/units.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
// some stations prefixed do not provide all the necessary data
|
||||
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
||||
@@ -49,7 +50,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
candidateObservation = await safeJson(`${station.id}/observations`, {
|
||||
data: {
|
||||
limit: 2, // we need the two most recent observations to calculate pressure direction
|
||||
limit: 5, // we need the two most recent observations to calculate pressure direction, and to back fill any missing data
|
||||
},
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
@@ -231,7 +232,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
this.setAutoReload();
|
||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||
return new Promise((resolve) => {
|
||||
if (this.data) resolve(this.data);
|
||||
if (this.data) resolve({ data: this.data, parameters: this.weatherParameters });
|
||||
// data not available, put it into the data callback queue
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
@@ -266,7 +267,7 @@ const parseData = (data) => {
|
||||
const kilometersConverter = distanceKilometers();
|
||||
const pressureConverter = pressure();
|
||||
|
||||
const observations = data.features[0].properties;
|
||||
const observations = backfill(data.features);
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
data.Temperature = temperatureConverter(observations.temperature.value);
|
||||
@@ -306,6 +307,46 @@ const parseData = (data) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
// default to the latest data in the provided observations, but use older data if something is missing
|
||||
const backfill = (data) => {
|
||||
// make easy to use timestamps
|
||||
const sortedData = data.map((observation) => {
|
||||
observation.timestamp = DateTime.fromISO(observation.properties.timestamp);
|
||||
return observation;
|
||||
});
|
||||
|
||||
// sort by timestamp with [0] being the earliest
|
||||
sortedData.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
// create the result data
|
||||
const result = {};
|
||||
|
||||
// backfill each property
|
||||
Object.keys(sortedData[0].properties).forEach((key) => {
|
||||
// qualify the key (must have value)
|
||||
if (Object.hasOwn(sortedData[0].properties[key], 'value')) {
|
||||
// backfill this property
|
||||
result[key] = backfillProperty(sortedData, key);
|
||||
} else {
|
||||
// use the property as is
|
||||
result[key] = sortedData[0].properties[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// return the property with a value closest to the [0] index
|
||||
// reduce returns the first non-null value in the array
|
||||
const backfillProperty = (data, key) => data.reduce(
|
||||
(prev, cur) => {
|
||||
const curValue = cur.properties?.[key]?.value;
|
||||
if (prev.value === null && curValue !== null && curValue !== undefined) return cur.properties[key];
|
||||
return prev;
|
||||
},
|
||||
{ value: null }, // null is the default provided by the api
|
||||
);
|
||||
|
||||
const display = new CurrentWeather(1, 'current-weather');
|
||||
registerDisplay(display);
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ const incrementInterval = (force) => {
|
||||
|
||||
const drawScreen = async () => {
|
||||
// get the conditions
|
||||
const data = await getCurrentWeather();
|
||||
const { data, parameters } = await getCurrentWeather();
|
||||
|
||||
// create a data object (empty if no valid current weather conditions)
|
||||
const scrollData = data || {};
|
||||
@@ -100,7 +100,7 @@ const drawScreen = async () => {
|
||||
// if we have no current weather and no hazards, there's nothing to display
|
||||
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
|
||||
|
||||
const thisScreen = workingScreens[screenIndex](scrollData);
|
||||
const thisScreen = workingScreens[screenIndex](scrollData, parameters);
|
||||
|
||||
// update classes on the scroll area
|
||||
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import STATUS from './status.mjs';
|
||||
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
||||
import { safeJson } from './utils/fetch.mjs';
|
||||
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
|
||||
import { temperature as temperatureUnit, windSpeed as windUnit } from './utils/units.mjs';
|
||||
import { getHourlyIcon } from './icons.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
@@ -191,7 +191,7 @@ class Hourly extends WeatherDisplay {
|
||||
const parseForecast = async (data) => {
|
||||
// get unit converters
|
||||
const temperatureConverter = temperatureUnit();
|
||||
const distanceConverter = distanceKilometers();
|
||||
const windConverter = windUnit();
|
||||
|
||||
// parse data
|
||||
const temperature = expand(data.temperature.values);
|
||||
@@ -210,8 +210,8 @@ const parseForecast = async (data) => {
|
||||
temperature: temperatureConverter(temperature[idx]),
|
||||
temperatureUnit: temperatureConverter.units,
|
||||
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
||||
windSpeed: distanceConverter(windSpeed[idx]),
|
||||
windUnit: distanceConverter.units,
|
||||
windSpeed: windConverter(windSpeed[idx]),
|
||||
windUnit: windConverter.units,
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { text } from './utils/fetch.mjs';
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { registerHiddenSetting } from './share.mjs';
|
||||
|
||||
let playlist;
|
||||
let currentTrack = 0;
|
||||
let player;
|
||||
let sliderTimeout = null;
|
||||
let volumeSlider = null;
|
||||
let volumeSliderInput = null;
|
||||
|
||||
const mediaPlaying = new Setting('mediaPlaying', {
|
||||
name: 'Media Playing',
|
||||
@@ -14,9 +18,24 @@ const mediaPlaying = new Setting('mediaPlaying', {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add the event handler to the page
|
||||
document.getElementById('ToggleMedia').addEventListener('click', toggleMedia);
|
||||
document.getElementById('ToggleMedia').addEventListener('click', handleClick);
|
||||
// get the slider elements
|
||||
volumeSlider = document.querySelector('#ToggleMediaContainer .volume-slider');
|
||||
volumeSliderInput = volumeSlider.querySelector('input');
|
||||
|
||||
// catch interactions with the volume slider (timeout handler)
|
||||
// called on any interaction via 'input' (vs change) for immediate volume response
|
||||
volumeSlider.addEventListener('input', setSliderTimeout);
|
||||
volumeSlider.addEventListener('input', sliderChanged);
|
||||
|
||||
// add listener for mute (pause) button under the volume slider
|
||||
volumeSlider.querySelector('img').addEventListener('click', stopMedia);
|
||||
|
||||
// get the playlist
|
||||
getMedia();
|
||||
|
||||
// register the volume setting
|
||||
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
|
||||
});
|
||||
|
||||
const scanMusicDirectory = async () => {
|
||||
@@ -77,7 +96,7 @@ const enableMediaPlayer = () => {
|
||||
// randomize the list
|
||||
randomizePlaylist();
|
||||
// enable the icon
|
||||
const icon = document.getElementById('ToggleMedia');
|
||||
const icon = document.getElementById('ToggleMediaContainer');
|
||||
icon.classList.add('available');
|
||||
// set the button type
|
||||
setIcon();
|
||||
@@ -85,15 +104,12 @@ const enableMediaPlayer = () => {
|
||||
if (mediaPlaying.value === true) {
|
||||
startMedia();
|
||||
}
|
||||
// add the volume control to the page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.append(mediaVolume.generate());
|
||||
}
|
||||
};
|
||||
|
||||
const setIcon = () => {
|
||||
// get the icon
|
||||
const icon = document.getElementById('ToggleMedia');
|
||||
const icon = document.getElementById('ToggleMediaContainer');
|
||||
if (mediaPlaying.value === true) {
|
||||
icon.classList.add('playing');
|
||||
} else {
|
||||
@@ -101,18 +117,54 @@ const setIcon = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMedia = (forcedState) => {
|
||||
// handle forcing
|
||||
if (typeof forcedState === 'boolean') {
|
||||
mediaPlaying.value = forcedState;
|
||||
} else {
|
||||
// toggle the state
|
||||
mediaPlaying.value = !mediaPlaying.value;
|
||||
const handleClick = () => {
|
||||
// if media is off, start it
|
||||
if (mediaPlaying.value === false) {
|
||||
mediaPlaying.value = true;
|
||||
}
|
||||
|
||||
if (mediaPlaying.value === true && !volumeSlider.classList.contains('show')) {
|
||||
// if media is playing and the slider isn't open, open it
|
||||
showVolumeSlider();
|
||||
} else {
|
||||
// hide the volume slider
|
||||
hideVolumeSlider();
|
||||
}
|
||||
|
||||
// handle the state change
|
||||
stateChanged();
|
||||
};
|
||||
|
||||
// set a timeout for the volume slider (called by interactions with the slider)
|
||||
const setSliderTimeout = () => {
|
||||
// clear existing timeout
|
||||
if (sliderTimeout) clearTimeout(sliderTimeout);
|
||||
// set a new timeout
|
||||
sliderTimeout = setTimeout(hideVolumeSlider, 5000);
|
||||
};
|
||||
|
||||
// show the volume slider and configure a timeout
|
||||
const showVolumeSlider = () => {
|
||||
setSliderTimeout();
|
||||
|
||||
// show the slider
|
||||
if (volumeSlider) {
|
||||
volumeSlider.classList.add('show');
|
||||
}
|
||||
};
|
||||
|
||||
// hide the volume slider and clean up the timeout
|
||||
const hideVolumeSlider = () => {
|
||||
// clear the timeout handler
|
||||
if (sliderTimeout) clearTimeout(sliderTimeout);
|
||||
sliderTimeout = null;
|
||||
|
||||
// hide the element
|
||||
if (volumeSlider) {
|
||||
volumeSlider.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
const startMedia = async () => {
|
||||
// if there's not media player yet, enable it
|
||||
if (!player) {
|
||||
@@ -134,9 +186,12 @@ const startMedia = async () => {
|
||||
};
|
||||
|
||||
const stopMedia = () => {
|
||||
hideVolumeSlider();
|
||||
if (!player) return;
|
||||
player.pause();
|
||||
mediaPlaying.value = false;
|
||||
setTrackName('Not playing');
|
||||
setIcon();
|
||||
};
|
||||
|
||||
const stateChanged = () => {
|
||||
@@ -170,6 +225,16 @@ const setVolume = (newVolume) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sliderChanged = () => {
|
||||
// get the value of the slider
|
||||
if (volumeSlider) {
|
||||
const newValue = volumeSliderInput.value;
|
||||
const cleanValue = parseFloat(newValue) / 100;
|
||||
setVolume(cleanValue);
|
||||
mediaVolume.value = cleanValue;
|
||||
}
|
||||
};
|
||||
|
||||
const mediaVolume = new Setting('mediaVolume', {
|
||||
name: 'Volume',
|
||||
type: 'select',
|
||||
@@ -205,7 +270,9 @@ const initializePlayer = () => {
|
||||
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||
setTrackName(playlist.availableFiles[currentTrack]);
|
||||
player.type = 'audio/mpeg';
|
||||
// set volume and slider indicator
|
||||
setVolume(mediaVolume.value);
|
||||
volumeSliderInput.value = Math.round(mediaVolume.value * 100);
|
||||
};
|
||||
|
||||
const playerCanPlay = async () => {
|
||||
@@ -238,5 +305,5 @@ const setTrackName = (fileName) => {
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
toggleMedia,
|
||||
handleClick,
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
weatherParameters.forecast = point.properties.forecast;
|
||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||
weatherParameters.stations = stations.features;
|
||||
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
|
||||
|
||||
// update the main process for display purposes
|
||||
populateWeatherParameters(weatherParameters, point.properties);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { registerHiddenSetting } from './share.mjs';
|
||||
|
||||
// Initialize settings immediately so other modules can access them
|
||||
const settings = { speed: { value: 1.0 } };
|
||||
@@ -6,6 +7,11 @@ const settings = { speed: { value: 1.0 } };
|
||||
// Track settings that need DOM changes after early initialization
|
||||
const deferredDomSettings = new Set();
|
||||
|
||||
// don't show checkboxes for these settings
|
||||
const hiddenSettings = [
|
||||
'scanLines',
|
||||
];
|
||||
|
||||
// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ)
|
||||
const wideScreenChange = (value) => {
|
||||
const container = document.querySelector('#divTwc');
|
||||
@@ -63,13 +69,19 @@ const scanLineChange = (value) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const modeSelect = document.getElementById('settings-scanLineMode-label');
|
||||
|
||||
if (value) {
|
||||
container.classList.add('scanlines');
|
||||
navIcons.classList.add('on');
|
||||
modeSelect?.style?.removeProperty('display');
|
||||
} else {
|
||||
// Remove all scanline classes
|
||||
container.classList.remove('scanlines', 'scanlines-auto', 'scanlines-fine', 'scanlines-normal', 'scanlines-thick', 'scanlines-classic', 'scanlines-retro');
|
||||
navIcons.classList.remove('on');
|
||||
if (modeSelect) {
|
||||
modeSelect.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -206,10 +218,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// Then generate the settings UI
|
||||
const settingHtml = Object.values(settings).map((d) => d.generate());
|
||||
const settingHtml = Object.values(settings).map((setting) => {
|
||||
if (hiddenSettings.includes(setting.shortName)) {
|
||||
// setting is hidden, register it
|
||||
registerHiddenSetting(setting.elemId, setting);
|
||||
return false;
|
||||
}
|
||||
// generate HTML for setting
|
||||
return setting.generate();
|
||||
}).filter((d) => d);
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.innerHTML = '';
|
||||
settingsSection.append(...settingHtml);
|
||||
|
||||
// update visibility on some settings
|
||||
const modeSelect = document.getElementById('settings-scanLineMode-label');
|
||||
const { value } = settings.scanLines;
|
||||
if (value) {
|
||||
modeSelect?.style?.removeProperty('display');
|
||||
} else if (modeSelect) {
|
||||
modeSelect.style.display = 'none';
|
||||
}
|
||||
registerHiddenSetting('settings-scanLineMode-select', settings.scanLineMode);
|
||||
});
|
||||
|
||||
export default settings;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
import Setting from './utils/setting.mjs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => init());
|
||||
|
||||
// shorthand mappings for frequently used values
|
||||
const specialMappings = {
|
||||
kiosk: 'settings-kiosk-checkbox',
|
||||
};
|
||||
// array of settings that are not checkboxes or dropdowns (i.e. volume slider)
|
||||
const hiddenSettings = [];
|
||||
|
||||
const init = () => {
|
||||
// add action to existing link
|
||||
@@ -45,9 +44,15 @@ const createLink = async (e) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// add the location string
|
||||
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
|
||||
queryStringElements.latLon = localStorage.getItem('latLon');
|
||||
// get any hidden settings
|
||||
hiddenSettings.forEach((setting) => {
|
||||
// determine type
|
||||
if (setting.value instanceof Setting) {
|
||||
queryStringElements[setting.name] = setting.value.value;
|
||||
} else if (typeof setting.value === 'function') {
|
||||
queryStringElements[setting.name] = setting.value();
|
||||
}
|
||||
});
|
||||
|
||||
const queryString = (new URLSearchParams(queryStringElements)).toString();
|
||||
|
||||
@@ -90,29 +95,17 @@ const writeLinkToPage = (url) => {
|
||||
shareLinkUrl.select();
|
||||
};
|
||||
|
||||
const parseQueryString = () => {
|
||||
// return memoized result
|
||||
if (parseQueryString.params) return parseQueryString.params;
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// turn into an array of key-value pairs
|
||||
const paramsArray = [...urlSearchParams];
|
||||
|
||||
// add additional expanded keys
|
||||
paramsArray.forEach((paramPair) => {
|
||||
const expandedKey = specialMappings[paramPair[0]];
|
||||
if (expandedKey) {
|
||||
paramsArray.push([expandedKey, paramPair[1]]);
|
||||
}
|
||||
const registerHiddenSetting = (name, value) => {
|
||||
// name is the id of the element
|
||||
// value can be a function that returns the current value of the setting
|
||||
// or an instance of Setting
|
||||
hiddenSettings.push({
|
||||
name,
|
||||
value,
|
||||
});
|
||||
|
||||
// memoize result
|
||||
parseQueryString.params = Object.fromEntries(paramsArray);
|
||||
|
||||
return parseQueryString.params;
|
||||
};
|
||||
|
||||
export {
|
||||
createLink,
|
||||
parseQueryString,
|
||||
registerHiddenSetting,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { parseQueryString } from '../share.mjs';
|
||||
|
||||
const SETTINGS_KEY = 'Settings';
|
||||
|
||||
const DEFAULTS = {
|
||||
@@ -15,6 +13,11 @@ const DEFAULTS = {
|
||||
placeholder: '',
|
||||
};
|
||||
|
||||
// shorthand mappings for frequently used values
|
||||
const specialMappings = {
|
||||
kiosk: 'settings-kiosk-checkbox',
|
||||
};
|
||||
|
||||
class Setting {
|
||||
constructor(shortName, _options) {
|
||||
if (shortName === undefined) {
|
||||
@@ -35,9 +38,10 @@ class Setting {
|
||||
this.visible = options.visible;
|
||||
this.changeAction = options.changeAction;
|
||||
this.placeholder = options.placeholder;
|
||||
this.elemId = `settings-${shortName}-${this.type}`;
|
||||
|
||||
// get value from url
|
||||
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
||||
const urlValue = parseQueryString()?.[this.elemId];
|
||||
let urlState;
|
||||
if (this.type === 'checkbox' && urlValue !== undefined) {
|
||||
urlState = urlValue === 'true';
|
||||
@@ -254,7 +258,10 @@ class Setting {
|
||||
break;
|
||||
case 'checkbox':
|
||||
default:
|
||||
this.element.querySelector('input').checked = newValue;
|
||||
// allow for a hidden checkbox (typically items in the player control bar)
|
||||
if (this.element) {
|
||||
this.element.querySelector('input').checked = newValue;
|
||||
}
|
||||
}
|
||||
this.storeToLocalStorage(this.myValue);
|
||||
|
||||
@@ -285,4 +292,30 @@ class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
const parseQueryString = () => {
|
||||
// return memoized result
|
||||
if (parseQueryString.params) return parseQueryString.params;
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// turn into an array of key-value pairs
|
||||
const paramsArray = [...urlSearchParams];
|
||||
|
||||
// add additional expanded keys
|
||||
paramsArray.forEach((paramPair) => {
|
||||
const expandedKey = specialMappings[paramPair[0]];
|
||||
if (expandedKey) {
|
||||
paramsArray.push([expandedKey, paramPair[1]]);
|
||||
}
|
||||
});
|
||||
|
||||
// memoize result
|
||||
parseQueryString.params = Object.fromEntries(paramsArray);
|
||||
|
||||
return parseQueryString.params;
|
||||
};
|
||||
|
||||
export default Setting;
|
||||
|
||||
export {
|
||||
parseQueryString,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import {
|
||||
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
|
||||
} from './navigation.mjs';
|
||||
import { parseQueryString } from './share.mjs';
|
||||
import { parseQueryString } from './utils/setting.mjs';
|
||||
import settings from './settings.mjs';
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
|
||||
Reference in New Issue
Block a user