mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3fe3366c | ||
|
|
6f97e3d2b9 | ||
|
|
8255efd3f7 | ||
|
|
1c79b08228 | ||
|
|
66a161762e | ||
|
|
707b08ee1a |
@@ -136,7 +136,7 @@ services:
|
|||||||
# Each argument in the permalink URL can become an environment variable on the Docker host by adding WSQS_
|
# Each argument in the permalink URL can become an environment variable on the Docker host by adding WSQS_
|
||||||
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
|
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
|
||||||
# more complete list of configuration options.
|
# more complete list of configuration options.
|
||||||
- WSQS_latLonQuery="Orlando International Airport Orlando FL USA"
|
- WSQS_latLonQuery=Orlando International Airport Orlando FL USA
|
||||||
- WSQS_hazards_checkbox=false
|
- WSQS_hazards_checkbox=false
|
||||||
- WSQS_current_weather_checkbox=true
|
- WSQS_current_weather_checkbox=true
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import TerserPlugin from 'terser-webpack-plugin';
|
|||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import file from 'gulp-file';
|
import file from 'gulp-file';
|
||||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||||
|
import log from 'fancy-log';
|
||||||
import OVERRIDES from '../src/overrides.mjs';
|
import OVERRIDES from '../src/overrides.mjs';
|
||||||
|
|
||||||
// get cloudfront
|
// get cloudfront
|
||||||
@@ -204,11 +205,15 @@ const buildPlaylist = async () => {
|
|||||||
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logVersion = async () => {
|
||||||
|
log(`Version Published: ${version}`);
|
||||||
|
};
|
||||||
|
|
||||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||||
|
|
||||||
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
||||||
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
|
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
|
||||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
const publishFrontend = series(buildDist, uploadImages, upload, invalidate, logVersion);
|
||||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||||
|
|
||||||
export default publishFrontend;
|
export default publishFrontend;
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.2.7",
|
"version": "6.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.2.7",
|
"version": "6.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-plugin-import": "^2.10.0",
|
"eslint-plugin-import": "^2.10.0",
|
||||||
|
"fancy-log": "^2.0.0",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
"gulp-awspublish": "^8.0.0",
|
"gulp-awspublish": "^8.0.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.2.7",
|
"version": "6.3.0",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-plugin-import": "^2.10.0",
|
"eslint-plugin-import": "^2.10.0",
|
||||||
|
"fancy-log": "^2.0.0",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
"gulp-awspublish": "^8.0.0",
|
"gulp-awspublish": "^8.0.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
@@ -45,14 +46,14 @@
|
|||||||
"gulp-sass": "^6.0.0",
|
"gulp-sass": "^6.0.0",
|
||||||
"gulp-terser": "^2.0.0",
|
"gulp-terser": "^2.0.0",
|
||||||
"luxon": "^3.0.0",
|
"luxon": "^3.0.0",
|
||||||
|
"metar-taf-parser": "^9.0.0",
|
||||||
"nosleep.js": "^0.12.0",
|
"nosleep.js": "^0.12.0",
|
||||||
"sass": "^1.54.0",
|
"sass": "^1.54.0",
|
||||||
"suncalc": "^1.8.0",
|
"suncalc": "^1.8.0",
|
||||||
"swiped-events": "^1.1.4",
|
"swiped-events": "^1.1.4",
|
||||||
"terser-webpack-plugin": "^5.3.6",
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"webpack": "^5.99.9",
|
"webpack": "^5.99.9",
|
||||||
"webpack-stream": "^7.0.0",
|
"webpack-stream": "^7.0.0"
|
||||||
"metar-taf-parser": "^9.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, isIOS,
|
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, isIOS,
|
||||||
} from './modules/navigation.mjs';
|
} from './modules/navigation.mjs';
|
||||||
import { round2 } from './modules/utils/units.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 settings from './modules/settings.mjs';
|
||||||
import AutoComplete from './modules/autocomplete.mjs';
|
import AutoComplete from './modules/autocomplete.mjs';
|
||||||
import { loadAllData } from './modules/utils/data-loader.mjs';
|
import { loadAllData } from './modules/utils/data-loader.mjs';
|
||||||
import { debugFlag } from './modules/utils/debug.mjs';
|
import { debugFlag } from './modules/utils/debug.mjs';
|
||||||
|
import { parseQueryString } from './modules/utils/setting.mjs';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
init();
|
init();
|
||||||
@@ -131,7 +132,7 @@ const init = async () => {
|
|||||||
const { lat, lon } = JSON.parse(latLon);
|
const { lat, lon } = JSON.parse(latLon);
|
||||||
getForecastFromLatLon(lat, lon, true);
|
getForecastFromLatLon(lat, lon, true);
|
||||||
} else {
|
} else {
|
||||||
// otherwise use pre-stored data
|
// otherwise use pre-stored data
|
||||||
loadData(JSON.parse(latLon));
|
loadData(JSON.parse(latLon));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +178,10 @@ const init = async () => {
|
|||||||
// swipe functionality
|
// swipe functionality
|
||||||
document.querySelector('#container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
document.querySelector('#container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
||||||
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
|
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) => {
|
const geocodeLatLonQuery = async (query) => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from './utils/units.mjs';
|
} from './utils/units.mjs';
|
||||||
import { debugFlag } from './utils/debug.mjs';
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.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
|
// 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'];
|
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
|
// eslint-disable-next-line no-await-in-loop
|
||||||
candidateObservation = await safeJson(`${station.id}/observations`, {
|
candidateObservation = await safeJson(`${station.id}/observations`, {
|
||||||
data: {
|
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,
|
retryCount: 3,
|
||||||
stillWaiting: () => this.stillWaiting(),
|
stillWaiting: () => this.stillWaiting(),
|
||||||
@@ -266,7 +267,7 @@ const parseData = (data) => {
|
|||||||
const kilometersConverter = distanceKilometers();
|
const kilometersConverter = distanceKilometers();
|
||||||
const pressureConverter = pressure();
|
const pressureConverter = pressure();
|
||||||
|
|
||||||
const observations = data.features[0].properties;
|
const observations = backfill(data.features);
|
||||||
// values from api are provided in metric
|
// values from api are provided in metric
|
||||||
data.observations = observations;
|
data.observations = observations;
|
||||||
data.Temperature = temperatureConverter(observations.temperature.value);
|
data.Temperature = temperatureConverter(observations.temperature.value);
|
||||||
@@ -306,6 +307,46 @@ const parseData = (data) => {
|
|||||||
return 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');
|
const display = new CurrentWeather(1, 'current-weather');
|
||||||
registerDisplay(display);
|
registerDisplay(display);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { text } from './utils/fetch.mjs';
|
import { text } from './utils/fetch.mjs';
|
||||||
import Setting from './utils/setting.mjs';
|
import Setting from './utils/setting.mjs';
|
||||||
|
import { registerHiddenSetting } from './share.mjs';
|
||||||
|
|
||||||
let playlist;
|
let playlist;
|
||||||
let currentTrack = 0;
|
let currentTrack = 0;
|
||||||
let player;
|
let player;
|
||||||
|
let sliderTimeout = null;
|
||||||
|
let volumeSlider = null;
|
||||||
|
let volumeSliderInput = null;
|
||||||
|
|
||||||
const mediaPlaying = new Setting('mediaPlaying', {
|
const mediaPlaying = new Setting('mediaPlaying', {
|
||||||
name: 'Media Playing',
|
name: 'Media Playing',
|
||||||
@@ -14,9 +18,24 @@ const mediaPlaying = new Setting('mediaPlaying', {
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// add the event handler to the page
|
// 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
|
// get the playlist
|
||||||
getMedia();
|
getMedia();
|
||||||
|
|
||||||
|
// register the volume setting
|
||||||
|
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
|
||||||
});
|
});
|
||||||
|
|
||||||
const scanMusicDirectory = async () => {
|
const scanMusicDirectory = async () => {
|
||||||
@@ -77,7 +96,7 @@ const enableMediaPlayer = () => {
|
|||||||
// randomize the list
|
// randomize the list
|
||||||
randomizePlaylist();
|
randomizePlaylist();
|
||||||
// enable the icon
|
// enable the icon
|
||||||
const icon = document.getElementById('ToggleMedia');
|
const icon = document.getElementById('ToggleMediaContainer');
|
||||||
icon.classList.add('available');
|
icon.classList.add('available');
|
||||||
// set the button type
|
// set the button type
|
||||||
setIcon();
|
setIcon();
|
||||||
@@ -85,15 +104,12 @@ const enableMediaPlayer = () => {
|
|||||||
if (mediaPlaying.value === true) {
|
if (mediaPlaying.value === true) {
|
||||||
startMedia();
|
startMedia();
|
||||||
}
|
}
|
||||||
// add the volume control to the page
|
|
||||||
const settingsSection = document.querySelector('#settings');
|
|
||||||
settingsSection.append(mediaVolume.generate());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setIcon = () => {
|
const setIcon = () => {
|
||||||
// get the icon
|
// get the icon
|
||||||
const icon = document.getElementById('ToggleMedia');
|
const icon = document.getElementById('ToggleMediaContainer');
|
||||||
if (mediaPlaying.value === true) {
|
if (mediaPlaying.value === true) {
|
||||||
icon.classList.add('playing');
|
icon.classList.add('playing');
|
||||||
} else {
|
} else {
|
||||||
@@ -101,18 +117,54 @@ const setIcon = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMedia = (forcedState) => {
|
const handleClick = () => {
|
||||||
// handle forcing
|
// if media is off, start it
|
||||||
if (typeof forcedState === 'boolean') {
|
if (mediaPlaying.value === false) {
|
||||||
mediaPlaying.value = forcedState;
|
mediaPlaying.value = true;
|
||||||
} else {
|
|
||||||
// toggle the state
|
|
||||||
mediaPlaying.value = !mediaPlaying.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// handle the state change
|
||||||
stateChanged();
|
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 () => {
|
const startMedia = async () => {
|
||||||
// if there's not media player yet, enable it
|
// if there's not media player yet, enable it
|
||||||
if (!player) {
|
if (!player) {
|
||||||
@@ -134,9 +186,12 @@ const startMedia = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stopMedia = () => {
|
const stopMedia = () => {
|
||||||
|
hideVolumeSlider();
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
player.pause();
|
player.pause();
|
||||||
|
mediaPlaying.value = false;
|
||||||
setTrackName('Not playing');
|
setTrackName('Not playing');
|
||||||
|
setIcon();
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateChanged = () => {
|
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', {
|
const mediaVolume = new Setting('mediaVolume', {
|
||||||
name: 'Volume',
|
name: 'Volume',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -205,7 +270,9 @@ const initializePlayer = () => {
|
|||||||
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||||
setTrackName(playlist.availableFiles[currentTrack]);
|
setTrackName(playlist.availableFiles[currentTrack]);
|
||||||
player.type = 'audio/mpeg';
|
player.type = 'audio/mpeg';
|
||||||
|
// set volume and slider indicator
|
||||||
setVolume(mediaVolume.value);
|
setVolume(mediaVolume.value);
|
||||||
|
volumeSliderInput.value = Math.round(mediaVolume.value * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playerCanPlay = async () => {
|
const playerCanPlay = async () => {
|
||||||
@@ -238,5 +305,5 @@ const setTrackName = (fileName) => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
toggleMedia,
|
handleClick,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Setting from './utils/setting.mjs';
|
import Setting from './utils/setting.mjs';
|
||||||
|
import { registerHiddenSetting } from './share.mjs';
|
||||||
|
|
||||||
// Initialize settings immediately so other modules can access them
|
// Initialize settings immediately so other modules can access them
|
||||||
const settings = { speed: { value: 1.0 } };
|
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
|
// Track settings that need DOM changes after early initialization
|
||||||
const deferredDomSettings = new Set();
|
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)
|
// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ)
|
||||||
const wideScreenChange = (value) => {
|
const wideScreenChange = (value) => {
|
||||||
const container = document.querySelector('#divTwc');
|
const container = document.querySelector('#divTwc');
|
||||||
@@ -63,13 +69,19 @@ const scanLineChange = (value) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modeSelect = document.getElementById('settings-scanLineMode-label');
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
container.classList.add('scanlines');
|
container.classList.add('scanlines');
|
||||||
navIcons.classList.add('on');
|
navIcons.classList.add('on');
|
||||||
|
modeSelect?.style?.removeProperty('display');
|
||||||
} else {
|
} else {
|
||||||
// Remove all scanline classes
|
// Remove all scanline classes
|
||||||
container.classList.remove('scanlines', 'scanlines-auto', 'scanlines-fine', 'scanlines-normal', 'scanlines-thick', 'scanlines-classic', 'scanlines-retro');
|
container.classList.remove('scanlines', 'scanlines-auto', 'scanlines-fine', 'scanlines-normal', 'scanlines-thick', 'scanlines-classic', 'scanlines-retro');
|
||||||
navIcons.classList.remove('on');
|
navIcons.classList.remove('on');
|
||||||
|
if (modeSelect) {
|
||||||
|
modeSelect.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,10 +218,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Then generate the settings UI
|
// 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');
|
const settingsSection = document.querySelector('#settings');
|
||||||
settingsSection.innerHTML = '';
|
settingsSection.innerHTML = '';
|
||||||
settingsSection.append(...settingHtml);
|
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;
|
export default settings;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { elemForEach } from './utils/elem.mjs';
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
|
import Setting from './utils/setting.mjs';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => init());
|
document.addEventListener('DOMContentLoaded', () => init());
|
||||||
|
|
||||||
// shorthand mappings for frequently used values
|
// array of settings that are not checkboxes or dropdowns (i.e. volume slider)
|
||||||
const specialMappings = {
|
const hiddenSettings = [];
|
||||||
kiosk: 'settings-kiosk-checkbox',
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
// add action to existing link
|
// add action to existing link
|
||||||
@@ -45,9 +44,15 @@ const createLink = async (e) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// add the location string
|
// get any hidden settings
|
||||||
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
|
hiddenSettings.forEach((setting) => {
|
||||||
queryStringElements.latLon = localStorage.getItem('latLon');
|
// 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();
|
const queryString = (new URLSearchParams(queryStringElements)).toString();
|
||||||
|
|
||||||
@@ -90,29 +95,17 @@ const writeLinkToPage = (url) => {
|
|||||||
shareLinkUrl.select();
|
shareLinkUrl.select();
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseQueryString = () => {
|
const registerHiddenSetting = (name, value) => {
|
||||||
// return memoized result
|
// name is the id of the element
|
||||||
if (parseQueryString.params) return parseQueryString.params;
|
// value can be a function that returns the current value of the setting
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
// or an instance of Setting
|
||||||
|
hiddenSettings.push({
|
||||||
// turn into an array of key-value pairs
|
name,
|
||||||
const paramsArray = [...urlSearchParams];
|
value,
|
||||||
|
|
||||||
// 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 {
|
export {
|
||||||
createLink,
|
createLink,
|
||||||
parseQueryString,
|
registerHiddenSetting,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { parseQueryString } from '../share.mjs';
|
|
||||||
|
|
||||||
const SETTINGS_KEY = 'Settings';
|
const SETTINGS_KEY = 'Settings';
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
@@ -15,6 +13,11 @@ const DEFAULTS = {
|
|||||||
placeholder: '',
|
placeholder: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// shorthand mappings for frequently used values
|
||||||
|
const specialMappings = {
|
||||||
|
kiosk: 'settings-kiosk-checkbox',
|
||||||
|
};
|
||||||
|
|
||||||
class Setting {
|
class Setting {
|
||||||
constructor(shortName, _options) {
|
constructor(shortName, _options) {
|
||||||
if (shortName === undefined) {
|
if (shortName === undefined) {
|
||||||
@@ -35,9 +38,10 @@ class Setting {
|
|||||||
this.visible = options.visible;
|
this.visible = options.visible;
|
||||||
this.changeAction = options.changeAction;
|
this.changeAction = options.changeAction;
|
||||||
this.placeholder = options.placeholder;
|
this.placeholder = options.placeholder;
|
||||||
|
this.elemId = `settings-${shortName}-${this.type}`;
|
||||||
|
|
||||||
// get value from url
|
// get value from url
|
||||||
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
const urlValue = parseQueryString()?.[this.elemId];
|
||||||
let urlState;
|
let urlState;
|
||||||
if (this.type === 'checkbox' && urlValue !== undefined) {
|
if (this.type === 'checkbox' && urlValue !== undefined) {
|
||||||
urlState = urlValue === 'true';
|
urlState = urlValue === 'true';
|
||||||
@@ -254,7 +258,10 @@ class Setting {
|
|||||||
break;
|
break;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
default:
|
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);
|
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 default Setting;
|
||||||
|
|
||||||
|
export {
|
||||||
|
parseQueryString,
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
|
|||||||
import {
|
import {
|
||||||
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
|
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
|
||||||
} from './navigation.mjs';
|
} from './navigation.mjs';
|
||||||
import { parseQueryString } from './share.mjs';
|
import { parseQueryString } from './utils/setting.mjs';
|
||||||
import settings from './settings.mjs';
|
import settings from './settings.mjs';
|
||||||
import { elemForEach } from './utils/elem.mjs';
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
import { debugFlag } from './utils/debug.mjs';
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,8 +2,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ToggleMedia {
|
#ToggleMediaContainer {
|
||||||
display: none;
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.available {
|
&.available {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -31,4 +32,31 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
width: 100%;
|
||||||
|
background-color: #000;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -815,4 +815,10 @@ body.kiosk #loading .instructions {
|
|||||||
>*:not(#divTwc) {
|
>*:not(#divTwc) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#divInfo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
@@ -147,9 +147,15 @@
|
|||||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
|
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
|
||||||
</div>
|
</div>
|
||||||
<div id="divTwcBottomRight">
|
<div id="divTwcBottomRight">
|
||||||
<div id="ToggleMedia">
|
<div id="ToggleMediaContainer">
|
||||||
<img class="navButton off" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Unmute" />
|
<div id="ToggleMedia">
|
||||||
<img class="navButton on" src="images/nav/ic_volume_on_white_24dp_2x.png" title="Mute" />
|
<img class="navButton off" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Unmute" />
|
||||||
|
<img class="navButton on" src="images/nav/ic_volume_on_white_24dp_2x.png" title="Volume" />
|
||||||
|
</div>
|
||||||
|
<div class="volume-slider">
|
||||||
|
<input type="range" min="1" max="100" value="75" /><br>
|
||||||
|
<img class="navButton" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Mute" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ToggleScanlines">
|
<div id="ToggleScanlines">
|
||||||
<img class="navButton off" src="images/nav/ic_scanlines_off_white_24dp_2x.png" title="Scan lines on" />
|
<img class="navButton off" src="images/nav/ic_scanlines_off_white_24dp_2x.png" title="Scan lines on" />
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"unmuted",
|
"unmuted",
|
||||||
"dumpio",
|
"dumpio",
|
||||||
"mesonet",
|
"mesonet",
|
||||||
"metar"
|
"metar",
|
||||||
|
"Unmute"
|
||||||
],
|
],
|
||||||
"cSpell.ignorePaths": [
|
"cSpell.ignorePaths": [
|
||||||
"**/package-lock.json",
|
"**/package-lock.json",
|
||||||
|
|||||||
Reference in New Issue
Block a user