mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 07:39:29 -07:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194e108037 | ||
|
|
d5b7c6630a | ||
|
|
39bafae394 | ||
|
|
ec8fffbb64 | ||
|
|
0a794eae36 | ||
|
|
8c83736aba | ||
|
|
872162080d | ||
|
|
69d2b0f40b | ||
|
|
37193112a7 | ||
|
|
0d9c445919 | ||
|
|
6c9fb4cf68 | ||
|
|
59b10ae222 | ||
|
|
d18b13821a | ||
|
|
320d3139c3 | ||
|
|
34dedb44c1 | ||
|
|
18633708f9 | ||
|
|
9b12255e0a | ||
|
|
f3360772c8 | ||
|
|
767bb8f11d | ||
|
|
7586dd7489 | ||
|
|
f37cbd66f7 | ||
|
|
d00262ebbc | ||
|
|
b4646b128a | ||
|
|
9f78761fe8 | ||
|
|
31c060c6d9 | ||
|
|
770f671d45 | ||
|
|
da3fe3366c | ||
|
|
6f97e3d2b9 | ||
|
|
8255efd3f7 | ||
|
|
1c79b08228 | ||
|
|
66a161762e | ||
|
|
707b08ee1a |
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -2,7 +2,7 @@
|
||||
"liveSassCompile.settings.formats": [
|
||||
{
|
||||
"format": "compressed",
|
||||
"extensionName": ".css",
|
||||
"extensionName": ".min.css",
|
||||
"savePath": "/server/styles",
|
||||
}
|
||||
],
|
||||
@@ -17,4 +17,4 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
}
|
||||
}
|
||||
22
README.md
22
README.md
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# WeatherStar 4000+
|
||||
|
||||
A live version of this project is available at https://weatherstar.netbymatt.com
|
||||
@@ -136,7 +138,7 @@ services:
|
||||
# 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
|
||||
# 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_current_weather_checkbox=true
|
||||
ports:
|
||||
@@ -200,7 +202,9 @@ https://weatherstar.netbymatt.com/?settings-units-select=metric
|
||||
```
|
||||
|
||||
### Kiosk mode
|
||||
Kiosk mode can be activated by a checkbox on the page. Note that there is no way out of kiosk mode (except refresh or closing the browser), and the play/pause and other controls will not be available. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified. A separate full-screen icon is available in the tool bar to go full-screen on a laptop or mobile browser.
|
||||
Kiosk mode can be activated by a checkbox on the page. This will start Weatherstar in a fullscreen-like view without the play/volume/etc toolbar and scaled to fill the entire space. This does not activate the browser's fullscreen or kiosk mode. Those can only be activated by user interaction or by launching the browser with specific parameters such as `--start-fullscreen` or `--kiosk`.
|
||||
|
||||
When using kiosk mode (via the checkbox), there will be no way to exit the fullscreen-like view of weatherstar. Reloading the page should remove the kiosk checkbox and return you to the normal view. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified. A separate full-screen icon is available in the tool bar to go full-screen on a laptop or mobile browser.
|
||||
|
||||
It's also possible to enter kiosk mode using a permalink. First generate a [Permalink](#sharing-a-permalink-bookmarking), then to the end of it add `&kiosk=true`. Opening this link will load all of the selected displays included in the Permalink, enter kiosk mode immediately upon loading and start playing the forecast.
|
||||
|
||||
@@ -332,8 +336,10 @@ When using Docker:
|
||||
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
|
||||
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
|
||||
|
||||
### RSS feeds and custom scroll
|
||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set.
|
||||
### Custom text scroll
|
||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, turn on the setting for `Enable RSS Feed/Text` and then enter text in the resulting text box. Then press set.
|
||||
|
||||
Tip: You can have Weatherstar select randomly between several text strings on each pass through the current conditions. Use a pipe character to separate string. `Welcome to Weatherstar|Thanks for watching`.
|
||||
|
||||
## Issue reporting and feature requests
|
||||
|
||||
@@ -347,6 +353,14 @@ Note: not all units are converted to metric, if selected. Some text-based produc
|
||||
|
||||
This is a known problem with the Ws4kp as it ages. It was a problem with the [actual Weatherstar hardware](https://youtu.be/rcUwlZ4pqh0?feature=shared&t=116) as well.
|
||||
|
||||
## Phone App
|
||||
|
||||
An Android app is in a closed beta test. It's nothing too special, just a wrapper for displaying the website in a browser.
|
||||
|
||||
You can get this functionality without an app on both Andriod and iOS by using the install or add to home screen feature of your browser.
|
||||
|
||||
iOS native app? No. I own zero Apple devices and thus have no way to develop, test, compile or verify myself to the app store. That application will have to come from the community.
|
||||
|
||||
## Related Projects
|
||||
|
||||
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
||||
|
||||
@@ -729,6 +729,16 @@
|
||||
"wfo": "LMK"
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "Lubbock",
|
||||
"lat": 33.5836,
|
||||
"lon": -101.8549,
|
||||
"point": {
|
||||
"x": 49,
|
||||
"y": 34,
|
||||
"wfo": "LUB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "Manchester",
|
||||
"lat": 42.9956,
|
||||
|
||||
@@ -364,6 +364,11 @@
|
||||
"lat": 38.2542,
|
||||
"lon": -85.7594
|
||||
},
|
||||
{
|
||||
"city": "Lubbock",
|
||||
"lat": 33.5836,
|
||||
"lon": -101.8549
|
||||
},
|
||||
{
|
||||
"city": "Manchester",
|
||||
"lat": 42.9956,
|
||||
|
||||
@@ -14,11 +14,17 @@ import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { readFile } from 'fs/promises';
|
||||
import file from 'gulp-file';
|
||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||
import log from 'fancy-log';
|
||||
import dartSass from 'sass';
|
||||
import gulpSass from 'gulp-sass';
|
||||
import sourceMaps from 'gulp-sourcemaps';
|
||||
import OVERRIDES from '../src/overrides.mjs';
|
||||
|
||||
// get cloudfront
|
||||
import reader from '../src/playlist-reader.mjs';
|
||||
|
||||
const sass = gulpSass(dartSass);
|
||||
|
||||
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||
|
||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||
@@ -35,6 +41,7 @@ const webpackOptions = {
|
||||
resolve: {
|
||||
roots: ['./'],
|
||||
},
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
@@ -79,7 +86,7 @@ const mjsSources = [
|
||||
'server/scripts/modules/travelforecast.mjs',
|
||||
'server/scripts/modules/progress.mjs',
|
||||
'server/scripts/modules/media.mjs',
|
||||
'server/scripts/modules/custom-rss-feed.mjs',
|
||||
'server/scripts/modules/custom-scroll-text.mjs',
|
||||
'server/scripts/index.mjs',
|
||||
];
|
||||
|
||||
@@ -88,10 +95,13 @@ const buildJs = () => src(mjsSources)
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const cssSources = [
|
||||
'server/styles/main.css',
|
||||
'server/styles/scss/**/*.scss',
|
||||
];
|
||||
const copyCss = () => src(cssSources)
|
||||
.pipe(concat('ws.min.css'))
|
||||
const buildCss = () => src(cssSources)
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(sass({ style: 'compressed' }).on('error', sass.logError))
|
||||
.pipe(rename({ suffix: '.min' }))
|
||||
.pipe(sourceMaps.write('./'))
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const htmlSources = [
|
||||
@@ -140,7 +150,6 @@ const s3 = s3Upload({
|
||||
});
|
||||
const uploadSources = [
|
||||
'dist/**',
|
||||
'!dist/**/*.map',
|
||||
'!dist/images/**/*',
|
||||
'!dist/fonts/**/*',
|
||||
];
|
||||
@@ -204,11 +213,15 @@ const buildPlaylist = async () => {
|
||||
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
||||
};
|
||||
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||
const logVersion = async () => {
|
||||
log(`Version Published: ${version}`);
|
||||
};
|
||||
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, buildCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||
|
||||
// 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
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate, logVersion);
|
||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||
|
||||
export default publishFrontend;
|
||||
|
||||
@@ -158,6 +158,7 @@ if (process.env?.DIST === '1') {
|
||||
// 'npm run build' and then 'DIST=1 npm start'
|
||||
app.use('/scripts', express.static('./server/scripts', staticOptions));
|
||||
app.use('/geoip', geoip);
|
||||
app.use('/music', express.static('./server/music', staticOptions));
|
||||
|
||||
// render the EJS template in production mode (serve compressed files from dist directory)
|
||||
app.get('/', (req, res) => { renderIndex(req, res, true); });
|
||||
|
||||
1960
package-lock.json
generated
1960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "6.2.7",
|
||||
"version": "6.5.1",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -34,8 +34,9 @@
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-awspublish": "^8.0.0",
|
||||
"gulp-awspublish": "^9.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-file": "^0.4.0",
|
||||
@@ -43,16 +44,17 @@
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-s3-uploader": "^1.0.6",
|
||||
"gulp-sass": "^6.0.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-terser": "^2.0.0",
|
||||
"luxon": "^3.0.0",
|
||||
"metar-taf-parser": "^9.0.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"sass": "^1.54.0",
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-stream": "^7.0.0",
|
||||
"metar-taf-parser": "^9.0.0"
|
||||
"webpack-stream": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.0.1",
|
||||
|
||||
@@ -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();
|
||||
@@ -131,7 +132,7 @@ const init = async () => {
|
||||
const { lat, lon } = JSON.parse(latLon);
|
||||
getForecastFromLatLon(lat, lon, true);
|
||||
} else {
|
||||
// otherwise use pre-stored data
|
||||
// otherwise use pre-stored data
|
||||
loadData(JSON.parse(latLon));
|
||||
}
|
||||
}
|
||||
@@ -177,6 +178,10 @@ 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) => {
|
||||
|
||||
@@ -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(),
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
// change of enable handler
|
||||
const changeEnable = (newValue) => {
|
||||
let newDisplay;
|
||||
if (newValue) {
|
||||
// add the feed to the scroll
|
||||
parseFeed(customFeed.value);
|
||||
// show the string box
|
||||
newDisplay = 'block';
|
||||
} else {
|
||||
// set scroll back to original
|
||||
resetScroll();
|
||||
// hide the string entry
|
||||
newDisplay = 'none';
|
||||
}
|
||||
const stringEntry = document.getElementById('settings-customFeed-label');
|
||||
if (stringEntry) {
|
||||
stringEntry.style.display = newDisplay;
|
||||
}
|
||||
};
|
||||
|
||||
// parse the feed/text provided
|
||||
const parseFeed = (textInput) => {
|
||||
// skip getting the feed on first run
|
||||
if (firstRun) return;
|
||||
|
||||
// test validity
|
||||
if (textInput === undefined || textInput === '') {
|
||||
resetScroll();
|
||||
}
|
||||
|
||||
// test for url
|
||||
if (textInput.match(/https?:\/\//)) {
|
||||
getFeed(textInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// add single text scroll after hazards if present
|
||||
resetScroll();
|
||||
addScroll(hazards);
|
||||
addScroll(
|
||||
() => (
|
||||
{
|
||||
type: 'scroll',
|
||||
text: textInput,
|
||||
}),
|
||||
// keep the existing scroll
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
// get the rss feed and then swap out the current weather scroll
|
||||
const getFeed = async (url) => {
|
||||
// get the text as a string
|
||||
// it needs to be proxied, use a free service
|
||||
const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`);
|
||||
|
||||
// this returns a data url
|
||||
// a few sanity checks
|
||||
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
|
||||
// determine return type
|
||||
const isBase64 = rssResponse.status.content_type.substring(0, 8) !== 'text/xml';
|
||||
|
||||
// base 64 decode everything after the comma
|
||||
const rss = isBase64 ? atob(rssResponse.contents.split('base64,')[1]) : rssResponse.contents;
|
||||
|
||||
// parse the rss
|
||||
const doc = parser.parseFromString(rss, 'text/xml');
|
||||
|
||||
// get the title
|
||||
const rssTitle = doc.querySelector('channel title').textContent;
|
||||
|
||||
// get each item
|
||||
const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent);
|
||||
|
||||
// reset the scroll, then add the screens
|
||||
resetScroll();
|
||||
// add the hazards scroll first
|
||||
addScroll(hazards);
|
||||
titles.forEach((title) => {
|
||||
// data is provided to the screen handler, so we return a function
|
||||
addScroll(
|
||||
() => ({
|
||||
header: rssTitle,
|
||||
type: 'scroll',
|
||||
text: title,
|
||||
}),
|
||||
// false parameter does not include the default weather scrolls
|
||||
false,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// change the feed source and re-load if necessary
|
||||
const changeFeed = (newValue) => {
|
||||
// first pass through won't have custom feed enable ready
|
||||
if (firstRun) return;
|
||||
|
||||
if (customFeedEnable.value) {
|
||||
parseFeed(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const customFeed = new Setting('customFeed', {
|
||||
name: 'Custom RSS Feed',
|
||||
defaultValue: '',
|
||||
type: 'string',
|
||||
changeAction: changeFeed,
|
||||
placeholder: 'Text or URL',
|
||||
});
|
||||
|
||||
const customFeedEnable = new Setting('customFeedEnable', {
|
||||
name: 'Enable RSS Feed/Text',
|
||||
defaultValue: false,
|
||||
changeAction: changeEnable,
|
||||
});
|
||||
|
||||
// initialize the custom feed inputs on the page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add the controls to the page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.append(customFeedEnable.generate(), customFeed.generate());
|
||||
// clear the first run value
|
||||
firstRun = false;
|
||||
// call change enable with the current value to show/hide the url box
|
||||
// and make the call to get the feed if enabled
|
||||
changeEnable(customFeedEnable.value);
|
||||
});
|
||||
91
server/scripts/modules/custom-scroll-text.mjs
Normal file
91
server/scripts/modules/custom-scroll-text.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
// change of enable handler
|
||||
const changeEnable = (newValue) => {
|
||||
let newDisplay;
|
||||
if (newValue) {
|
||||
// add the text to the scroll
|
||||
parseText(customText.value);
|
||||
// show the string box
|
||||
newDisplay = 'block';
|
||||
} else {
|
||||
// set scroll back to original
|
||||
resetScroll();
|
||||
// hide the string entry
|
||||
newDisplay = 'none';
|
||||
}
|
||||
const stringEntry = document.getElementById('settings-customText-label');
|
||||
if (stringEntry) {
|
||||
stringEntry.style.display = newDisplay;
|
||||
}
|
||||
};
|
||||
|
||||
// parse the text provided
|
||||
const parseText = (textInput) => {
|
||||
// skip updating text on first run
|
||||
if (firstRun) return;
|
||||
|
||||
// test validity
|
||||
if (textInput === undefined || textInput === '') {
|
||||
resetScroll();
|
||||
}
|
||||
|
||||
// split the text at pipe characters
|
||||
const texts = textInput.split('|');
|
||||
|
||||
// add single text scroll after hazards if present
|
||||
resetScroll();
|
||||
addScroll(hazards);
|
||||
addScroll(
|
||||
() => {
|
||||
// pick a random string from the available list
|
||||
const randInt = Math.floor(Math.random() * texts.length);
|
||||
return {
|
||||
type: 'scroll',
|
||||
text: texts[randInt],
|
||||
};
|
||||
},
|
||||
// keep the existing scroll
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
// change the text
|
||||
const changeText = (newValue) => {
|
||||
// first pass through won't have custom text enable ready
|
||||
if (firstRun) return;
|
||||
|
||||
if (customTextEnable.value) {
|
||||
parseText(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const customText = new Setting('customText', {
|
||||
name: 'Custom Text',
|
||||
defaultValue: '',
|
||||
type: 'string',
|
||||
changeAction: changeText,
|
||||
placeholder: 'Text to scroll',
|
||||
});
|
||||
|
||||
const customTextEnable = new Setting('customTextEnable', {
|
||||
name: 'Enable Custom Text',
|
||||
defaultValue: false,
|
||||
changeAction: changeEnable,
|
||||
});
|
||||
|
||||
// initialize the custom text inputs on the page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add the controls to the page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.append(customTextEnable.generate(), customText.generate());
|
||||
// clear the first run value
|
||||
firstRun = false;
|
||||
// call change enable with the current value to show/hide the url box
|
||||
changeEnable(customTextEnable.value);
|
||||
});
|
||||
@@ -40,9 +40,10 @@ class HourlyGraph extends WeatherDisplay {
|
||||
const temperature = data.map((d) => d.temperature);
|
||||
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
||||
const skyCover = data.map((d) => d.skyCover);
|
||||
const dewpoint = data.map((d) => d.dewpoint);
|
||||
|
||||
this.data = {
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
||||
};
|
||||
|
||||
this.setStatus(STATUS.loaded);
|
||||
@@ -63,12 +64,16 @@ class HourlyGraph extends WeatherDisplay {
|
||||
|
||||
// calculate time scale
|
||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
||||
const timeStep = this.data.temperature.length / 4;
|
||||
const startTime = DateTime.now().startOf('hour');
|
||||
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
|
||||
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
|
||||
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
|
||||
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
|
||||
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
|
||||
let prevTime = startTime;
|
||||
Array(5).fill().forEach((val, idx) => {
|
||||
// track the previous label so a day of week can be added when it changes
|
||||
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
||||
prevTime = label.ts;
|
||||
// write to page
|
||||
document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted;
|
||||
});
|
||||
|
||||
// order is important last line drawn is on top
|
||||
// clouds
|
||||
@@ -86,11 +91,22 @@ class HourlyGraph extends WeatherDisplay {
|
||||
lineWidth: 3,
|
||||
});
|
||||
|
||||
// calculate temperature scale for min and max of dewpoint and temperature
|
||||
const minScale = Math.min(...this.data.dewpoint, ...this.data.temperature);
|
||||
const maxScale = Math.max(...this.data.dewpoint, ...this.data.temperature);
|
||||
const thirdScale = (maxScale - minScale) / 3;
|
||||
const midScale1 = Math.round(minScale + thirdScale);
|
||||
const midScale2 = Math.round(minScale + (thirdScale * 2));
|
||||
const tempScale = calcScale(minScale, availableHeight - 10, maxScale, 10);
|
||||
|
||||
// dewpoint
|
||||
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
|
||||
drawPath(dewpointPath, ctx, {
|
||||
strokeStyle: 'green',
|
||||
lineWidth: 3,
|
||||
});
|
||||
|
||||
// temperature
|
||||
const minTemp = Math.min(...this.data.temperature);
|
||||
const maxTemp = Math.max(...this.data.temperature);
|
||||
const midTemp = Math.round((minTemp + maxTemp) / 2);
|
||||
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
|
||||
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
|
||||
drawPath(tempPath, ctx, {
|
||||
strokeStyle: 'red',
|
||||
@@ -100,15 +116,17 @@ class HourlyGraph extends WeatherDisplay {
|
||||
// temperature axis labels
|
||||
// limited to 3 characters, sacraficing degree character
|
||||
const degree = String.fromCharCode(176);
|
||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-2').innerHTML = (midScale2 + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-3').innerHTML = (midScale1 + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-4').innerHTML = (minScale + degree).substring(0, 3);
|
||||
|
||||
// set the image source
|
||||
this.image.src = canvas.toDataURL();
|
||||
|
||||
// change the units in the header
|
||||
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||
this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||
|
||||
super.drawCanvas();
|
||||
this.finishDraw();
|
||||
@@ -145,7 +163,18 @@ const drawPath = (path, ctx, options) => {
|
||||
};
|
||||
|
||||
// format as 1p, 12a, etc.
|
||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||
const formatTime = (time, prev) => {
|
||||
// if the day of the week changes, show the day of the week in the label
|
||||
let format = 'ha';
|
||||
if (prev.weekday !== time.weekday) format = 'ccc ha';
|
||||
|
||||
const ts = time.setZone(timeZone());
|
||||
|
||||
return {
|
||||
ts,
|
||||
formatted: ts.toFormat(format).slice(0, -1),
|
||||
};
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
|
||||
@@ -75,7 +75,10 @@ class Hourly extends WeatherDisplay {
|
||||
|
||||
const startingHour = DateTime.local().setZone(timeZone());
|
||||
|
||||
const lines = this.data.map((data, index) => {
|
||||
// shorten to 24 hours
|
||||
const shortData = this.data.slice(0, 24);
|
||||
|
||||
const lines = shortData.map((data, index) => {
|
||||
const fillValues = {};
|
||||
// hour
|
||||
const hour = startingHour.plus({ hours: index });
|
||||
@@ -102,7 +105,7 @@ class Hourly extends WeatherDisplay {
|
||||
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
||||
|
||||
// alter the color of the feels like column to reflect wind chill or heat index
|
||||
if (feelsLike < temperature) {
|
||||
if (data.apparentTemperature < data.temperature) {
|
||||
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||
} else if (feelsLike > temperature) {
|
||||
filledRow.querySelector('.like').classList.add('heat-index');
|
||||
@@ -203,6 +206,7 @@ const parseForecast = async (data) => {
|
||||
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
||||
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
||||
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
|
||||
const dewpoint = expand(data.dewpoint.values);
|
||||
|
||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
@@ -216,6 +220,7 @@ const parseForecast = async (data) => {
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
icon: icons[idx],
|
||||
dewpoint: temperatureConverter(dewpoint[idx]),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -233,7 +238,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
|
||||
};
|
||||
|
||||
// expand a set of values with durations to an hour-by-hour array
|
||||
const expand = (data, maxHours = 24) => {
|
||||
const expand = (data, maxHours = 36) => {
|
||||
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
||||
const result = []; // resulting expanded values
|
||||
data.forEach((item) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
|
||||
} catch (error) {
|
||||
console.warn(`largeIcon: ${error.message}`);
|
||||
// Return a fallback icon to prevent downstream errors
|
||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
}
|
||||
|
||||
// find the icon
|
||||
@@ -102,6 +102,8 @@ const largeIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'winter_mix':
|
||||
case 'winter_mix-n':
|
||||
return addPath('Freezing-Rain-Snow.gif');
|
||||
|
||||
case 'fzra':
|
||||
@@ -141,6 +143,8 @@ const largeIcon = (link, _isNightTime) => {
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
case 'wind_':
|
||||
case 'wind_-n':
|
||||
return addPath('Windy.gif');
|
||||
|
||||
case 'wind_skc-n':
|
||||
@@ -169,7 +173,7 @@ const largeIcon = (link, _isNightTime) => {
|
||||
default: {
|
||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,6 +133,7 @@ const smallIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_few-n':
|
||||
case 'wind_':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_sct':
|
||||
@@ -170,7 +171,7 @@ const smallIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||
|
||||
// get a target distance
|
||||
let targetDistance = 2.5;
|
||||
let targetDistance = 2.4;
|
||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||
|
||||
// make station info into an array
|
||||
|
||||
@@ -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';
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
||||
right: 60px;
|
||||
width: 360px;
|
||||
font-family: 'Star4000 Small';
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
@include u.text-shadow();
|
||||
text-align: right;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
color: red;
|
||||
}
|
||||
|
||||
.dewpoint {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.cloud {
|
||||
color: lightgrey;
|
||||
}
|
||||
@@ -52,32 +56,33 @@
|
||||
|
||||
.x-axis {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 640px;
|
||||
left: 54px;
|
||||
width: 532px;
|
||||
height: 20px;
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
|
||||
&.l-1 {
|
||||
left: 25px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
&.l-2 {
|
||||
left: 158px;
|
||||
left: calc(532px / 4 * 1);
|
||||
}
|
||||
|
||||
&.l-3 {
|
||||
left: 291px;
|
||||
left: calc(532px / 4 * 2);
|
||||
}
|
||||
|
||||
&.l-4 {
|
||||
left: 424px;
|
||||
left: calc(532px / 4 * 3);
|
||||
}
|
||||
|
||||
&.l-5 {
|
||||
left: 557px;
|
||||
left: calc(532px / 4 * 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +115,14 @@
|
||||
}
|
||||
|
||||
&.l-2 {
|
||||
top: 140px;
|
||||
top: calc(280px / 3);
|
||||
}
|
||||
|
||||
&.l-3 {
|
||||
bottom: calc(280px / 3 - 11px);
|
||||
}
|
||||
|
||||
&.l-4 {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ToggleMedia {
|
||||
#ToggleMediaContainer {
|
||||
display: none;
|
||||
position: relative;
|
||||
|
||||
&.available {
|
||||
display: inline-block;
|
||||
@@ -31,4 +32,32 @@
|
||||
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
transform: translateY(-100%);
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
|
||||
@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) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
#divInfo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
max-width: 250px;
|
||||
}
|
||||
1
server/styles/ws.min.css
vendored
Normal file
1
server/styles/ws.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
server/styles/ws.min.css.map
Normal file
1
server/styles/ws.min.css.map
Normal file
File diff suppressed because one or more lines are too long
268
tests/package-lock.json
generated
268
tests/package-lock.json
generated
@@ -28,26 +28,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
||||
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz",
|
||||
"integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.1",
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.2",
|
||||
"tar-fs": "^3.0.8",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -64,13 +64,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
||||
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -135,28 +135,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
|
||||
"license": "Apache-2.0"
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
||||
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
|
||||
"integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.1.tgz",
|
||||
"integrity": "sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4"
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
@@ -171,9 +188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
|
||||
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -191,9 +208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
|
||||
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -212,6 +229,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||
@@ -240,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
@@ -252,9 +279,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
|
||||
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-11.0.0.tgz",
|
||||
"integrity": "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
@@ -332,9 +359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -363,10 +390,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1452169",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
|
||||
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
|
||||
"license": "BSD-3-Clause"
|
||||
"version": "0.0.1521046",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
@@ -375,9 +403,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
@@ -393,9 +421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
@@ -462,6 +490,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -522,9 +559,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-uri": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
|
||||
"integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
|
||||
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-ftp": "^5.0.2",
|
||||
@@ -578,14 +615,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
@@ -612,9 +645,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -623,12 +656,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
@@ -789,9 +816,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@@ -799,17 +826,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.0.tgz",
|
||||
"integrity": "sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==",
|
||||
"version": "24.31.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.31.0.tgz",
|
||||
"integrity": "sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.5",
|
||||
"chromium-bidi": "5.1.0",
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "11.0.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"devtools-protocol": "0.0.1452169",
|
||||
"puppeteer-core": "24.10.0",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"puppeteer-core": "24.31.0",
|
||||
"typed-query-selector": "^2.12.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -820,17 +847,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz",
|
||||
"integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==",
|
||||
"version": "24.31.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.31.0.tgz",
|
||||
"integrity": "sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.5",
|
||||
"chromium-bidi": "5.1.0",
|
||||
"debug": "^4.4.1",
|
||||
"devtools-protocol": "0.0.1452169",
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "11.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"ws": "^8.18.2"
|
||||
"webdriver-bidi-protocol": "0.3.9",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -855,9 +883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -877,12 +905,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -914,23 +942,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-events": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
@@ -960,9 +980,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
||||
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
@@ -1006,12 +1026,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.9.tgz",
|
||||
"integrity": "sha512-uIYvlRQ0PwtZR1EzHlTMol1G0lAlmOe6wPykF9a77AK3bkpvZHzIVxRE2ThOx5vjy2zISe0zhwf5rzuUfbo1PQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -1036,9 +1062,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -1103,9 +1129,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.49",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz",
|
||||
"integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==",
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
const OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||
</script>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="styles/ws.min.css" />
|
||||
<!--<script type="text/javascript">const OVERRIDES={};</script>-->
|
||||
<script type="text/javascript">
|
||||
OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||
@@ -62,7 +62,7 @@
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
||||
<script type="module" src="scripts/modules/custom-scroll-text.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
<% } %>
|
||||
|
||||
@@ -147,9 +147,15 @@
|
||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
|
||||
</div>
|
||||
<div id="divTwcBottomRight">
|
||||
<div id="ToggleMedia">
|
||||
<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="Mute" />
|
||||
<div id="ToggleMediaContainer">
|
||||
<div id="ToggleMedia">
|
||||
<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 id="ToggleScanlines">
|
||||
<img class="navButton off" src="images/nav/ic_scanlines_off_white_24dp_2x.png" title="Scan lines on" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="main has-scroll hourly-graph">
|
||||
<div class="top-right template ">
|
||||
<div class="temperature">Temperature</div>
|
||||
<div class="dewpoint">Dewpoint</div>
|
||||
<div class="cloud">Cloud %</div>
|
||||
<div class="rain">Precip %</div>
|
||||
</div>
|
||||
@@ -9,6 +10,7 @@
|
||||
<div class="label l-1">75</div>
|
||||
<div class="label l-2">65</div>
|
||||
<div class="label l-3">55</div>
|
||||
<div class="label l-4">45</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<img id="chart-area"></img>
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
"unmuted",
|
||||
"dumpio",
|
||||
"mesonet",
|
||||
"metar"
|
||||
"metar",
|
||||
"Unmute"
|
||||
],
|
||||
"cSpell.ignorePaths": [
|
||||
"**/package-lock.json",
|
||||
|
||||
Reference in New Issue
Block a user