mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37193112a7 | ||
|
|
0d9c445919 | ||
|
|
6c9fb4cf68 | ||
|
|
59b10ae222 | ||
|
|
d18b13821a | ||
|
|
320d3139c3 | ||
|
|
34dedb44c1 | ||
|
|
18633708f9 | ||
|
|
9b12255e0a | ||
|
|
f3360772c8 | ||
|
|
767bb8f11d | ||
|
|
7586dd7489 | ||
|
|
f37cbd66f7 | ||
|
|
d00262ebbc | ||
|
|
b4646b128a | ||
|
|
9f78761fe8 | ||
|
|
31c060c6d9 | ||
|
|
770f671d45 | ||
|
|
da3fe3366c | ||
|
|
6f97e3d2b9 | ||
|
|
8255efd3f7 | ||
|
|
1c79b08228 | ||
|
|
66a161762e | ||
|
|
707b08ee1a | ||
|
|
7900e59aab | ||
|
|
9b422dd697 | ||
|
|
e4ce0b6cc6 | ||
|
|
b0e5018179 | ||
|
|
6422589b5c | ||
|
|
407da90f8a | ||
|
|
3a0e6aa345 | ||
|
|
650dda7b61 | ||
|
|
8f1e8ffb74 | ||
|
|
93af84cbd8 | ||
|
|
117f66e9d0 | ||
|
|
bca9376edc | ||
|
|
8b076db25d | ||
|
|
807932fe3c | ||
|
|
7bb024eff5 | ||
|
|
f4a1a3a1d8 | ||
|
|
9a5efe9d48 | ||
|
|
58e0611a46 | ||
|
|
9ed496c892 | ||
|
|
31315d1ace | ||
|
|
77838e1a81 | ||
|
|
64d6484bd8 | ||
|
|
20cab8c25e | ||
|
|
b4de17ccd0 | ||
|
|
0fd90feb7a | ||
|
|
8c3b596b69 | ||
|
|
e57b9bcb20 | ||
|
|
e27750e915 | ||
|
|
f5431a04c7 | ||
|
|
5117a9d475 | ||
|
|
28baa022a9 | ||
|
|
e8b8890260 | ||
|
|
b797a10b9e | ||
|
|
2a64cda383 | ||
|
|
e6e357c51b | ||
|
|
24deb4dce4 | ||
|
|
14b1891efd | ||
|
|
f17f69f60e | ||
|
|
fa16095355 | ||
|
|
cc3dbeb043 | ||
|
|
8ee1e954eb | ||
|
|
bfc4bddfef | ||
|
|
567325e3c5 | ||
|
|
4903b95fec | ||
|
|
b43fb32820 | ||
|
|
0d0c4ec452 | ||
|
|
49d18c2fbe | ||
|
|
1732a3381f | ||
|
|
cc05aafb95 | ||
|
|
093b6ac239 | ||
|
|
12d068d740 | ||
|
|
517c560ef6 | ||
|
|
3eb571bed4 |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,4 +11,5 @@ Please do not report issues with api.weather.gov being down. It's a new service
|
|||||||
|
|
||||||
Please include:
|
Please include:
|
||||||
* Web browser and OS
|
* Web browser and OS
|
||||||
* Forecast Information text block from the very bottom of the web page
|
* Headend Information text block from the very bottom of the web page
|
||||||
|
* How you're running Weatherstar (Node, Dockerfile, Dockerfile.server, etc.)
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"liveSassCompile.settings.formats": [
|
"liveSassCompile.settings.formats": [
|
||||||
{
|
{
|
||||||
"format": "compressed",
|
"format": "compressed",
|
||||||
"extensionName": ".css",
|
"extensionName": ".min.css",
|
||||||
"savePath": "/server/styles",
|
"savePath": "/server/styles",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -17,4 +17,4 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ FROM node:24-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --omit=dev --legacy-peer-deps
|
RUN npm ci --legacy-peer-deps
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -1,3 +1,5 @@
|
|||||||
|

|
||||||
|
|
||||||
# WeatherStar 4000+
|
# WeatherStar 4000+
|
||||||
|
|
||||||
A live version of this project is available at https://weatherstar.netbymatt.com
|
A live version of this project is available at https://weatherstar.netbymatt.com
|
||||||
@@ -32,6 +34,18 @@ From a learning standpoint, this codebase make use of a lot of different methods
|
|||||||
* Hand written CSS made easier to mange with SASS
|
* Hand written CSS made easier to mange with SASS
|
||||||
* A linting library to keep code style consistent
|
* A linting library to keep code style consistent
|
||||||
|
|
||||||
|
## Quck Start
|
||||||
|
|
||||||
|
Ensure you have Node installed.
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/netbymatt/ws4kp.git
|
||||||
|
cd ws4kp
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Open your browser and navigate to https://localhost:8080
|
||||||
|
|
||||||
## Does WeatherStar 4000+ work outside of the USA?
|
## Does WeatherStar 4000+ work outside of the USA?
|
||||||
|
|
||||||
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclusive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclusive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
||||||
@@ -57,14 +71,7 @@ WeatherStar 4000+ supports two deployment modes:
|
|||||||
* Browser-based caching
|
* Browser-based caching
|
||||||
* Used by: static file hosting and default `Dockerfile`
|
* Used by: static file hosting and default `Dockerfile`
|
||||||
|
|
||||||
## Run Your WeatherStar
|
## Other methods to run Ws4kp
|
||||||
|
|
||||||
Ensure you have Node installed. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/netbymatt/ws4kp.git
|
|
||||||
cd ws4kp
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Mode (individual JS files, easier debugging)
|
### Development Mode (individual JS files, easier debugging)
|
||||||
```bash
|
```bash
|
||||||
@@ -131,7 +138,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:
|
||||||
@@ -174,7 +181,7 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
|
|||||||
* Radar displays the timestamp of the image.
|
* Radar displays the timestamp of the image.
|
||||||
* A new hour-by-hour graph of the temperature, cloud cover and precipitation chances for the next 24 hours.
|
* A new hour-by-hour graph of the temperature, cloud cover and precipitation chances for the next 24 hours.
|
||||||
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast. (off by default because it duplicates the hourly graph)
|
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast. (off by default because it duplicates the hourly graph)
|
||||||
* The SPC Outlook is shown in the style of the old air quality screen. This shows the probability of severe weather over the next 3 days at your location.
|
* The SPC Outlook is shown in the style of the old air quality screen. This shows the probability of severe weather over the next 3 days at your location. SPC outlook only displays if you're within one of the highlight areas over the next 3 day. You can view the [maps](https://www.weather.gov/crh/outlooks) and pick a location within one of the risk categories to see if the screen is working for you.
|
||||||
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90s.
|
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90s.
|
||||||
* The original music has been replaced. More info in [Music](#music).
|
* The original music has been replaced. More info in [Music](#music).
|
||||||
* Marine forecast (tides) is not available as it is not reliably part of the new API.
|
* Marine forecast (tides) is not available as it is not reliably part of the new API.
|
||||||
@@ -195,7 +202,9 @@ https://weatherstar.netbymatt.com/?settings-units-select=metric
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Kiosk mode
|
### 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.
|
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.
|
||||||
|
|
||||||
@@ -309,11 +318,14 @@ If you're unable to pre-set the play state before entering kiosk mode (such as w
|
|||||||
|
|
||||||
## Community Notes
|
## Community Notes
|
||||||
|
|
||||||
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
|
Thanks to the WeatherStar+ community for providing these discussions to further extend your retro forecasts!
|
||||||
|
|
||||||
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
|
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
|
||||||
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
|
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
|
||||||
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
|
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
|
||||||
|
* [SSL Certificates](https://github.com/netbymatt/ws4kp/issues/135) Discussion about how to host with an SSL certificate (enables geolocation).
|
||||||
|
* [Changing playlists](https://github.com/netbymatt/ws4kp/issues/138) Possible ways to automatically change the playlist on a schedule.
|
||||||
|
* [Customize Travel Forecast Cities](https://github.com/netbymatt/ws4kp/issues/146#issuecomment-3363940202)
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
@@ -339,6 +351,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.
|
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
|
## Related Projects
|
||||||
|
|
||||||
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
||||||
|
|||||||
@@ -729,6 +729,16 @@
|
|||||||
"wfo": "LMK"
|
"wfo": "LMK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"city": "Lubbock",
|
||||||
|
"lat": 33.5836,
|
||||||
|
"lon": -101.8549,
|
||||||
|
"point": {
|
||||||
|
"x": 49,
|
||||||
|
"y": 34,
|
||||||
|
"wfo": "LUB"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"city": "Manchester",
|
"city": "Manchester",
|
||||||
"lat": 42.9956,
|
"lat": 42.9956,
|
||||||
|
|||||||
@@ -84,8 +84,8 @@
|
|||||||
"Latitude": 29.7633,
|
"Latitude": 29.7633,
|
||||||
"Longitude": -95.3633,
|
"Longitude": -95.3633,
|
||||||
"point": {
|
"point": {
|
||||||
"x": 65,
|
"x": 63,
|
||||||
"y": 97,
|
"y": 95,
|
||||||
"wfo": "HGX"
|
"wfo": "HGX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -364,6 +364,11 @@
|
|||||||
"lat": 38.2542,
|
"lat": 38.2542,
|
||||||
"lon": -85.7594
|
"lon": -85.7594
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"city": "Lubbock",
|
||||||
|
"lat": 33.5836,
|
||||||
|
"lon": -101.8549
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"city": "Manchester",
|
"city": "Manchester",
|
||||||
"lat": 42.9956,
|
"lat": 42.9956,
|
||||||
|
|||||||
@@ -14,11 +14,17 @@ 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 dartSass from 'sass';
|
||||||
|
import gulpSass from 'gulp-sass';
|
||||||
|
import sourceMaps from 'gulp-sourcemaps';
|
||||||
import OVERRIDES from '../src/overrides.mjs';
|
import OVERRIDES from '../src/overrides.mjs';
|
||||||
|
|
||||||
// get cloudfront
|
// get cloudfront
|
||||||
import reader from '../src/playlist-reader.mjs';
|
import reader from '../src/playlist-reader.mjs';
|
||||||
|
|
||||||
|
const sass = gulpSass(dartSass);
|
||||||
|
|
||||||
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||||
|
|
||||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||||
@@ -35,6 +41,7 @@ const webpackOptions = {
|
|||||||
resolve: {
|
resolve: {
|
||||||
roots: ['./'],
|
roots: ['./'],
|
||||||
},
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: true,
|
minimize: true,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
@@ -88,32 +95,39 @@ const buildJs = () => src(mjsSources)
|
|||||||
.pipe(dest(RESOURCES_PATH));
|
.pipe(dest(RESOURCES_PATH));
|
||||||
|
|
||||||
const cssSources = [
|
const cssSources = [
|
||||||
'server/styles/main.css',
|
'server/styles/scss/**/*.scss',
|
||||||
];
|
];
|
||||||
const copyCss = () => src(cssSources)
|
const buildCss = () => src(cssSources)
|
||||||
.pipe(concat('ws.min.css'))
|
.pipe(sourceMaps.init())
|
||||||
|
.pipe(sass({ style: 'compressed' }).on('error', sass.logError))
|
||||||
|
.pipe(rename({ suffix: '.min' }))
|
||||||
|
.pipe(sourceMaps.write('./'))
|
||||||
.pipe(dest(RESOURCES_PATH));
|
.pipe(dest(RESOURCES_PATH));
|
||||||
|
|
||||||
const htmlSources = [
|
const htmlSources = [
|
||||||
'views/*.ejs',
|
'views/*.ejs',
|
||||||
];
|
];
|
||||||
const compressHtml = async () => {
|
const packageJson = await readFile('package.json');
|
||||||
const packageJson = await readFile('package.json');
|
let { version } = JSON.parse(packageJson);
|
||||||
const { version } = JSON.parse(packageJson);
|
const previewVersion = async () => {
|
||||||
|
// generate a relatively unique timestamp for cache invalidation of the preview site
|
||||||
return src(htmlSources)
|
const now = new Date();
|
||||||
.pipe(ejs({
|
const msNow = now.getTime() % 1_000_000;
|
||||||
production: version,
|
version = msNow.toString();
|
||||||
serverAvailable: false,
|
|
||||||
version,
|
|
||||||
OVERRIDES,
|
|
||||||
query: {},
|
|
||||||
}))
|
|
||||||
.pipe(rename({ extname: '.html' }))
|
|
||||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
|
||||||
.pipe(dest('./dist'));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const compressHtml = async () => src(htmlSources)
|
||||||
|
.pipe(ejs({
|
||||||
|
production: version,
|
||||||
|
serverAvailable: false,
|
||||||
|
version,
|
||||||
|
OVERRIDES,
|
||||||
|
query: {},
|
||||||
|
}))
|
||||||
|
.pipe(rename({ extname: '.html' }))
|
||||||
|
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||||
|
.pipe(dest('./dist'));
|
||||||
|
|
||||||
const otherFiles = [
|
const otherFiles = [
|
||||||
'server/robots.txt',
|
'server/robots.txt',
|
||||||
'server/manifest.json',
|
'server/manifest.json',
|
||||||
@@ -136,7 +150,6 @@ const s3 = s3Upload({
|
|||||||
});
|
});
|
||||||
const uploadSources = [
|
const uploadSources = [
|
||||||
'dist/**',
|
'dist/**',
|
||||||
'!dist/**/*.map',
|
|
||||||
'!dist/images/**/*',
|
'!dist/images/**/*',
|
||||||
'!dist/fonts/**/*',
|
'!dist/fonts/**/*',
|
||||||
];
|
];
|
||||||
@@ -200,12 +213,16 @@ const buildPlaylist = async () => {
|
|||||||
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
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
|
// 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(buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||||
|
|
||||||
export default publishFrontend;
|
export default publishFrontend;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import rename from 'gulp-rename';
|
|||||||
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
|
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
|
||||||
|
|
||||||
const vendorFiles = [
|
const vendorFiles = [
|
||||||
'./node_modules/luxon/build/es6/luxon.js',
|
'./node_modules/luxon/build/es6/luxon.mjs',
|
||||||
'./node_modules/luxon/build/es6/luxon.js.map',
|
'./node_modules/luxon/build/es6/luxon.mjs.map',
|
||||||
'./node_modules/nosleep.js/dist/NoSleep.js',
|
'./node_modules/nosleep.js/dist/NoSleep.js',
|
||||||
'./node_modules/suncalc/suncalc.js',
|
'./node_modules/suncalc/suncalc.js',
|
||||||
'./node_modules/swiped-events/src/swiped-events.js',
|
'./node_modules/swiped-events/src/swiped-events.js',
|
||||||
@@ -23,7 +23,6 @@ const copy = () => src(vendorFiles)
|
|||||||
path.dirname = path.dirname.toLowerCase();
|
path.dirname = path.dirname.toLowerCase();
|
||||||
path.basename = path.basename.toLowerCase();
|
path.basename = path.basename.toLowerCase();
|
||||||
path.extname = path.extname.toLowerCase();
|
path.extname = path.extname.toLowerCase();
|
||||||
if (path.basename === 'luxon') path.extname = '.mjs';
|
|
||||||
}))
|
}))
|
||||||
.pipe(dest('./server/scripts/vendor/auto'));
|
.pipe(dest('./server/scripts/vendor/auto'));
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import playlist from './src/playlist.mjs';
|
import playlist from './src/playlist.mjs';
|
||||||
import OVERRIDES from './src/overrides.mjs';
|
import OVERRIDES from './src/overrides.mjs';
|
||||||
import cache from './proxy/cache.mjs';
|
import cache from './proxy/cache.mjs';
|
||||||
|
import devTools from './src/com.chrome.devtools.mjs';
|
||||||
|
|
||||||
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
||||||
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
||||||
@@ -168,6 +169,7 @@ if (process.env?.DIST === '1') {
|
|||||||
app.use('/geoip', geoip);
|
app.use('/geoip', geoip);
|
||||||
app.use('/resources', express.static('./server/scripts/modules'));
|
app.use('/resources', express.static('./server/scripts/modules'));
|
||||||
app.get('/', index);
|
app.get('/', index);
|
||||||
|
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
|
||||||
app.get('*name', express.static('./server', staticOptions));
|
app.get('*name', express.static('./server', staticOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ server {
|
|||||||
|
|
||||||
add_header X-Weatherstar true always;
|
add_header X-Weatherstar true always;
|
||||||
|
|
||||||
|
include /etc/nginx/includes/wsqs_redirect.conf;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
index redirect.html index.html index.htm;
|
index index.html index.htm;
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2439
package-lock.json
generated
2439
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.1.5",
|
"version": "6.4.2",
|
||||||
"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",
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"start": "node index.mjs",
|
"start": "node index.mjs",
|
||||||
"stop": "pkill -f 'node index.mjs' || echo 'No process found'",
|
"stop": "pkill -f 'node index.mjs' || echo 'No process found'",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build:travelcities": "node datagenerators/travelcities.mjs",
|
||||||
"build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css",
|
"build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css",
|
||||||
"build": "gulp buildDist",
|
"build": "gulp buildDist",
|
||||||
"lint": "eslint ./server/scripts/**/*.mjs ./proxy/**/*.mjs ./src/**/*.mjs *.mjs",
|
"lint": "eslint ./server/scripts/**/*.mjs ./proxy/**/*.mjs ./src/**/*.mjs *.mjs",
|
||||||
@@ -33,8 +34,9 @@
|
|||||||
"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": "^9.0.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-ejs": "^5.1.0",
|
"gulp-ejs": "^5.1.0",
|
||||||
"gulp-file": "^0.4.0",
|
"gulp-file": "^0.4.0",
|
||||||
@@ -42,8 +44,10 @@
|
|||||||
"gulp-rename": "^2.0.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-s3-uploader": "^1.0.6",
|
"gulp-s3-uploader": "^1.0.6",
|
||||||
"gulp-sass": "^6.0.0",
|
"gulp-sass": "^6.0.0",
|
||||||
|
"gulp-sourcemaps": "^3.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",
|
||||||
@@ -55,7 +59,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0"
|
||||||
"metar-taf-parser": "^9.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
server/images/backgrounds/7-wide.png
Normal file
BIN
server/images/backgrounds/7-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
server/images/backgrounds/7.png
Normal file
BIN
server/images/backgrounds/7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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();
|
||||||
@@ -106,17 +107,34 @@ const init = async () => {
|
|||||||
|
|
||||||
// attempt to parse the url parameters
|
// attempt to parse the url parameters
|
||||||
const parsedParameters = parseQueryString();
|
const parsedParameters = parseQueryString();
|
||||||
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
const loadFromParsed = !!parsedParameters.latLon;
|
||||||
|
|
||||||
// Auto load the parsed parameters and fall back to the previous query
|
// Auto load the parsed parameters and fall back to the previous query
|
||||||
const query = parsedParameters.latLonQuery ?? localStorage.getItem('latLonQuery');
|
const query = parsedParameters.latLonQuery ?? localStorage.getItem('latLonQuery');
|
||||||
const latLon = parsedParameters.latLon ?? localStorage.getItem('latLon');
|
const latLon = parsedParameters.latLon ?? localStorage.getItem('latLon');
|
||||||
const fromGPS = localStorage.getItem('latLonFromGPS') && !loadFromParsed;
|
const fromGPS = localStorage.getItem('latLonFromGPS') && !loadFromParsed;
|
||||||
|
|
||||||
if (query && latLon && !fromGPS) {
|
if (parsedParameters.latLonQuery && !parsedParameters.latLon) {
|
||||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||||
txtAddress.value = query;
|
txtAddress.value = parsedParameters.latLonQuery;
|
||||||
loadData(JSON.parse(latLon));
|
const geometry = await geocodeLatLonQuery(parsedParameters.latLonQuery);
|
||||||
|
if (geometry) {
|
||||||
|
doRedirectToGeometry(geometry);
|
||||||
|
}
|
||||||
|
} else if (latLon && !fromGPS) {
|
||||||
|
// update in-page search box if using cached data, or parsed parameter
|
||||||
|
if ((query && !loadFromParsed) || (parsedParameters.latLonQuery && loadFromParsed)) {
|
||||||
|
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||||
|
txtAddress.value = query;
|
||||||
|
}
|
||||||
|
// use lat-long lookup if that's all that was provided in the query string
|
||||||
|
if (loadFromParsed && parsedParameters.latLon && !parsedParameters.latLonQuery) {
|
||||||
|
const { lat, lon } = JSON.parse(latLon);
|
||||||
|
getForecastFromLatLon(lat, lon, true);
|
||||||
|
} else {
|
||||||
|
// otherwise use pre-stored data
|
||||||
|
loadData(JSON.parse(latLon));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (fromGPS) {
|
if (fromGPS) {
|
||||||
btnGetGpsClick();
|
btnGetGpsClick();
|
||||||
@@ -160,6 +178,30 @@ 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) => {
|
||||||
|
try {
|
||||||
|
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
|
||||||
|
data: {
|
||||||
|
text: query,
|
||||||
|
f: 'json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loc = data.locations?.[0];
|
||||||
|
if (loc) {
|
||||||
|
return loc.feature.geometry;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Geocoding failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const autocompleteOnSelect = async (suggestion) => {
|
const autocompleteOnSelect = async (suggestion) => {
|
||||||
|
|||||||
@@ -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(),
|
||||||
@@ -231,7 +232,7 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
this.setAutoReload();
|
this.setAutoReload();
|
||||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.data) resolve(this.data);
|
if (this.data) resolve({ data: this.data, parameters: this.weatherParameters });
|
||||||
// data not available, put it into the data callback queue
|
// data not available, put it into the data callback queue
|
||||||
this.getDataCallbacks.push(() => resolve(this.data));
|
this.getDataCallbacks.push(() => resolve(this.data));
|
||||||
});
|
});
|
||||||
@@ -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,5 +1,4 @@
|
|||||||
import { locationCleanup } from './utils/string.mjs';
|
import { locationCleanup } from './utils/string.mjs';
|
||||||
import { elemForEach } from './utils/elem.mjs';
|
|
||||||
import getCurrentWeather from './currentweather.mjs';
|
import getCurrentWeather from './currentweather.mjs';
|
||||||
import { currentDisplay } from './navigation.mjs';
|
import { currentDisplay } from './navigation.mjs';
|
||||||
import getHazards from './hazards.mjs';
|
import getHazards from './hazards.mjs';
|
||||||
@@ -12,6 +11,16 @@ const TICK_INTERVAL_MS = 500; // milliseconds per tick
|
|||||||
const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_MS);
|
const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_MS);
|
||||||
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
|
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
|
||||||
|
|
||||||
|
// items on page
|
||||||
|
let mainScroll;
|
||||||
|
let fixedScroll;
|
||||||
|
let header;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
mainScroll = document.querySelector('#container>.scroll');
|
||||||
|
fixedScroll = document.querySelector('#container>.scroll .fixed');
|
||||||
|
header = document.querySelector('#container>.scroll .scroll-header');
|
||||||
|
});
|
||||||
|
|
||||||
// local variables
|
// local variables
|
||||||
let interval;
|
let interval;
|
||||||
let screenIndex = 0;
|
let screenIndex = 0;
|
||||||
@@ -23,6 +32,8 @@ let defaultScreensLoaded = true;
|
|||||||
// start drawing conditions
|
// start drawing conditions
|
||||||
// reset starts from the first item in the text scroll list
|
// reset starts from the first item in the text scroll list
|
||||||
const start = () => {
|
const start = () => {
|
||||||
|
// show the block
|
||||||
|
show();
|
||||||
// if already started, draw the screen on a reset flag and return
|
// if already started, draw the screen on a reset flag and return
|
||||||
if (interval) {
|
if (interval) {
|
||||||
if (resetFlag) drawScreen();
|
if (resetFlag) drawScreen();
|
||||||
@@ -62,6 +73,7 @@ const incrementInterval = (force) => {
|
|||||||
const display = currentDisplay();
|
const display = currentDisplay();
|
||||||
if (!display?.okToDrawCurrentConditions) {
|
if (!display?.okToDrawCurrentConditions) {
|
||||||
stop(display?.elemId === 'progress');
|
stop(display?.elemId === 'progress');
|
||||||
|
hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
||||||
@@ -72,7 +84,7 @@ const incrementInterval = (force) => {
|
|||||||
|
|
||||||
const drawScreen = async () => {
|
const drawScreen = async () => {
|
||||||
// get the conditions
|
// get the conditions
|
||||||
const data = await getCurrentWeather();
|
const { data, parameters } = await getCurrentWeather();
|
||||||
|
|
||||||
// create a data object (empty if no valid current weather conditions)
|
// create a data object (empty if no valid current weather conditions)
|
||||||
const scrollData = data || {};
|
const scrollData = data || {};
|
||||||
@@ -88,15 +100,11 @@ const drawScreen = async () => {
|
|||||||
// if we have no current weather and no hazards, there's nothing to display
|
// if we have no current weather and no hazards, there's nothing to display
|
||||||
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
|
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
|
||||||
|
|
||||||
const thisScreen = workingScreens[screenIndex](scrollData);
|
const thisScreen = workingScreens[screenIndex](scrollData, parameters);
|
||||||
|
|
||||||
// update classes on the scroll area
|
// update classes on the scroll area
|
||||||
elemForEach('.weather-display .scroll', (elem) => {
|
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
|
||||||
elem.classList.forEach((cls) => { if (cls !== 'scroll') elem.classList.remove(cls); });
|
thisScreen?.classes?.forEach((cls) => mainScroll.classList.add(cls));
|
||||||
// no scroll on progress
|
|
||||||
if (elem.parentElement.id === 'progress-html') return;
|
|
||||||
thisScreen?.classes?.forEach((cls) => elem.classList.add(cls));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof thisScreen === 'string') {
|
if (typeof thisScreen === 'string') {
|
||||||
// only a string
|
// only a string
|
||||||
@@ -125,9 +133,7 @@ const hazards = (data) => {
|
|||||||
// test for data
|
// test for data
|
||||||
if (!data.hazards || data.hazards.length === 0) return false;
|
if (!data.hazards || data.hazards.length === 0) return false;
|
||||||
|
|
||||||
// since the hazard scroll element has no left/right margins, pad the beginning and end with non-breaking spaces
|
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
|
||||||
const padding = ' '.repeat(4);
|
|
||||||
const hazard = `${padding}${data.hazards[0].properties.event} ${data.hazards[0].properties.description}${padding}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: hazard,
|
text: hazard,
|
||||||
@@ -190,17 +196,12 @@ let workingScreens = [...baseScreens, ...additionalScreens];
|
|||||||
|
|
||||||
// internal draw function with preset parameters
|
// internal draw function with preset parameters
|
||||||
const drawCondition = (text) => {
|
const drawCondition = (text) => {
|
||||||
// update all html scroll elements
|
fixedScroll.innerHTML = text;
|
||||||
elemForEach('.weather-display .scroll .fixed', (elem) => {
|
|
||||||
elem.innerHTML = text;
|
|
||||||
});
|
|
||||||
setHeader('');
|
setHeader('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHeader = (text) => {
|
const setHeader = (text) => {
|
||||||
elemForEach('.weather-display .scroll .scroll-header', (elem) => {
|
header.innerHTML = text ?? '';
|
||||||
elem.innerHTML = text ?? '';
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// reset the screens back to the original set
|
// reset the screens back to the original set
|
||||||
@@ -223,14 +224,14 @@ const drawScrollCondition = (screen) => {
|
|||||||
scrollElement.classList.add('scroll-area');
|
scrollElement.classList.add('scroll-area');
|
||||||
scrollElement.innerHTML = screen.text;
|
scrollElement.innerHTML = screen.text;
|
||||||
// add it to the page to get the width
|
// add it to the page to get the width
|
||||||
document.querySelector('.weather-display .scroll .fixed').innerHTML = scrollElement.outerHTML;
|
fixedScroll.innerHTML = scrollElement.outerHTML;
|
||||||
// grab the width
|
// grab the width
|
||||||
const { scrollWidth, clientWidth } = document.querySelector('.weather-display .scroll .fixed .scroll-area');
|
const { scrollWidth, clientWidth } = document.querySelector('#container>.scroll .fixed .scroll-area');
|
||||||
|
|
||||||
// calculate the scroll distance and set a minimum scroll
|
// calculate the scroll distance and set a minimum scroll
|
||||||
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
|
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
|
||||||
// calculate the scroll time (scaled by global speed setting)
|
// calculate the scroll time (scaled by global speed setting), minimum 2s (4s when added to start and end delays)
|
||||||
const scrollTime = scrollDistance / SCROLL_SPEED * settings.speed.value;
|
const scrollTime = Math.max(scrollDistance / SCROLL_SPEED * settings.speed.value, 2);
|
||||||
// add 1 second pause at the end of the scroll animation
|
// add 1 second pause at the end of the scroll animation
|
||||||
const endPauseTime = 1.0;
|
const endPauseTime = 1.0;
|
||||||
const totalAnimationTime = scrollTime + endPauseTime;
|
const totalAnimationTime = scrollTime + endPauseTime;
|
||||||
@@ -246,17 +247,13 @@ const drawScrollCondition = (screen) => {
|
|||||||
scrollElement.style.backfaceVisibility = 'hidden'; // Force hardware acceleration
|
scrollElement.style.backfaceVisibility = 'hidden'; // Force hardware acceleration
|
||||||
scrollElement.style.perspective = '1000px'; // Enable 3D rendering context
|
scrollElement.style.perspective = '1000px'; // Enable 3D rendering context
|
||||||
|
|
||||||
elemForEach('.weather-display .scroll .fixed', (elem) => {
|
fixedScroll.innerHTML = '';
|
||||||
elem.innerHTML = '';
|
fixedScroll.append(scrollElement.cloneNode(true));
|
||||||
elem.append(scrollElement.cloneNode(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
// start the scroll after the specified delay
|
// start the scroll after the specified delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// change the transform to trigger the scroll
|
// change the transform to trigger the scroll
|
||||||
elemForEach('.weather-display .scroll .fixed .scroll-area', (elem) => {
|
document.querySelector('#container>.scroll .fixed .scroll-area').style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
|
||||||
elem.style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
|
|
||||||
});
|
|
||||||
}, startDelayTime * 1000);
|
}, startDelayTime * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,9 +261,19 @@ const parseMessage = (event) => {
|
|||||||
if (event?.data?.type === 'current-weather-scroll') {
|
if (event?.data?.type === 'current-weather-scroll') {
|
||||||
if (event.data?.method === 'start') start();
|
if (event.data?.method === 'start') start();
|
||||||
if (event.data?.method === 'reload') stop(true);
|
if (event.data?.method === 'reload') stop(true);
|
||||||
|
if (event.data?.method === 'show') show();
|
||||||
|
if (event.data?.method === 'hide') hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
mainScroll.style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
mainScroll.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
const screenCount = () => workingScreens.length;
|
const screenCount = () => workingScreens.length;
|
||||||
const atDefault = () => defaultScreensLoaded;
|
const atDefault = () => defaultScreensLoaded;
|
||||||
|
|
||||||
@@ -277,6 +284,8 @@ window.CurrentWeatherScroll = {
|
|||||||
addScreen,
|
addScreen,
|
||||||
reset,
|
reset,
|
||||||
start,
|
start,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
screenCount,
|
screenCount,
|
||||||
atDefault,
|
atDefault,
|
||||||
};
|
};
|
||||||
@@ -285,6 +294,9 @@ export {
|
|||||||
addScreen,
|
addScreen,
|
||||||
reset,
|
reset,
|
||||||
start,
|
start,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
screenCount,
|
screenCount,
|
||||||
atDefault,
|
atDefault,
|
||||||
|
hazards,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Setting from './utils/setting.mjs';
|
import Setting from './utils/setting.mjs';
|
||||||
import { reset as resetScroll, addScreen as addScroll } from './currentweatherscroll.mjs';
|
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
|
|
||||||
let firstRun = true;
|
let firstRun = true;
|
||||||
@@ -42,8 +42,9 @@ const parseFeed = (textInput) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add single text scroll
|
// add single text scroll after hazards if present
|
||||||
resetScroll();
|
resetScroll();
|
||||||
|
addScroll(hazards);
|
||||||
addScroll(
|
addScroll(
|
||||||
() => (
|
() => (
|
||||||
{
|
{
|
||||||
@@ -81,6 +82,8 @@ const getFeed = async (url) => {
|
|||||||
|
|
||||||
// reset the scroll, then add the screens
|
// reset the scroll, then add the screens
|
||||||
resetScroll();
|
resetScroll();
|
||||||
|
// add the hazards scroll first
|
||||||
|
addScroll(hazards);
|
||||||
titles.forEach((title) => {
|
titles.forEach((title) => {
|
||||||
// data is provided to the screen handler, so we return a function
|
// data is provided to the screen handler, so we return a function
|
||||||
addScroll(
|
addScroll(
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// get the forecast using centralized safe handling
|
// get the forecast using centralized safe handling
|
||||||
const url = new URL('https://api.weather.gov/alerts/active');
|
const url = new URL('https://api.weather.gov/alerts/active');
|
||||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||||
|
url.searchParams.append('status', 'actual');
|
||||||
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||||
|
|
||||||
if (!alerts) {
|
if (!alerts) {
|
||||||
@@ -103,7 +104,10 @@ class Hazards extends WeatherDisplay {
|
|||||||
// show alert indicator
|
// show alert indicator
|
||||||
if (unViewed > 0) alert.classList.add('show');
|
if (unViewed > 0) alert.classList.add('show');
|
||||||
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
|
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
|
||||||
this.drawLongCanvas();
|
// unless this has been disabled
|
||||||
|
if (this.isEnabled) {
|
||||||
|
this.drawLongCanvas();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
||||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
@@ -115,7 +119,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
this.getDataCallback();
|
this.getDataCallback();
|
||||||
|
|
||||||
if (!superResult) {
|
if (!superResult) {
|
||||||
this.setStatus(STATUS.loaded);
|
// Don't override status - super.getData() already set it to STATUS.disabled
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawLongCanvas();
|
this.drawLongCanvas();
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
const temperature = data.map((d) => d.temperature);
|
const temperature = data.map((d) => d.temperature);
|
||||||
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
||||||
const skyCover = data.map((d) => d.skyCover);
|
const skyCover = data.map((d) => d.skyCover);
|
||||||
|
const dewpoint = data.map((d) => d.dewpoint);
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
@@ -63,12 +64,16 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
|
|
||||||
// calculate time scale
|
// calculate time scale
|
||||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
||||||
|
const timeStep = this.data.temperature.length / 4;
|
||||||
const startTime = DateTime.now().startOf('hour');
|
const startTime = DateTime.now().startOf('hour');
|
||||||
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
|
let prevTime = startTime;
|
||||||
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
|
Array(5).fill().forEach((val, idx) => {
|
||||||
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
|
// track the previous label so a day of week can be added when it changes
|
||||||
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
|
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
||||||
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
|
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
|
// order is important last line drawn is on top
|
||||||
// clouds
|
// clouds
|
||||||
@@ -86,11 +91,22 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
lineWidth: 3,
|
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
|
// 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);
|
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
|
||||||
drawPath(tempPath, ctx, {
|
drawPath(tempPath, ctx, {
|
||||||
strokeStyle: 'red',
|
strokeStyle: 'red',
|
||||||
@@ -100,15 +116,17 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
// temperature axis labels
|
// temperature axis labels
|
||||||
// limited to 3 characters, sacraficing degree character
|
// limited to 3 characters, sacraficing degree character
|
||||||
const degree = String.fromCharCode(176);
|
const degree = String.fromCharCode(176);
|
||||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + 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 = (midTemp + 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 = (minTemp + 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
|
// set the image source
|
||||||
this.image.src = canvas.toDataURL();
|
this.image.src = canvas.toDataURL();
|
||||||
|
|
||||||
// change the units in the header
|
// change the units in the header
|
||||||
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
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();
|
super.drawCanvas();
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
@@ -145,7 +163,18 @@ const drawPath = (path, ctx, options) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// format as 1p, 12a, etc.
|
// 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
|
// register display
|
||||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
||||||
import { safeJson } from './utils/fetch.mjs';
|
import { safeJson } from './utils/fetch.mjs';
|
||||||
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
|
import { temperature as temperatureUnit, windSpeed as windUnit } from './utils/units.mjs';
|
||||||
import { getHourlyIcon } from './icons.mjs';
|
import { getHourlyIcon } from './icons.mjs';
|
||||||
import { directionToNSEW } from './utils/calc.mjs';
|
import { directionToNSEW } from './utils/calc.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
@@ -75,7 +75,10 @@ class Hourly extends WeatherDisplay {
|
|||||||
|
|
||||||
const startingHour = DateTime.local().setZone(timeZone());
|
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 = {};
|
const fillValues = {};
|
||||||
// hour
|
// hour
|
||||||
const hour = startingHour.plus({ hours: index });
|
const hour = startingHour.plus({ hours: index });
|
||||||
@@ -102,7 +105,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
||||||
|
|
||||||
// alter the color of the feels like column to reflect wind chill or heat index
|
// 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');
|
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||||
} else if (feelsLike > temperature) {
|
} else if (feelsLike > temperature) {
|
||||||
filledRow.querySelector('.like').classList.add('heat-index');
|
filledRow.querySelector('.like').classList.add('heat-index');
|
||||||
@@ -191,7 +194,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const parseForecast = async (data) => {
|
const parseForecast = async (data) => {
|
||||||
// get unit converters
|
// get unit converters
|
||||||
const temperatureConverter = temperatureUnit();
|
const temperatureConverter = temperatureUnit();
|
||||||
const distanceConverter = distanceKilometers();
|
const windConverter = windUnit();
|
||||||
|
|
||||||
// parse data
|
// parse data
|
||||||
const temperature = expand(data.temperature.values);
|
const temperature = expand(data.temperature.values);
|
||||||
@@ -203,6 +206,7 @@ const parseForecast = async (data) => {
|
|||||||
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
||||||
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
||||||
const snowfallAmount = expand(data.snowfallAmount.values); // snow 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);
|
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||||
|
|
||||||
@@ -210,12 +214,13 @@ const parseForecast = async (data) => {
|
|||||||
temperature: temperatureConverter(temperature[idx]),
|
temperature: temperatureConverter(temperature[idx]),
|
||||||
temperatureUnit: temperatureConverter.units,
|
temperatureUnit: temperatureConverter.units,
|
||||||
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
||||||
windSpeed: distanceConverter(windSpeed[idx]),
|
windSpeed: windConverter(windSpeed[idx]),
|
||||||
windUnit: distanceConverter.units,
|
windUnit: windConverter.units,
|
||||||
windDirection: directionToNSEW(windDirection[idx]),
|
windDirection: directionToNSEW(windDirection[idx]),
|
||||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||||
skyCover: skyCover[idx],
|
skyCover: skyCover[idx],
|
||||||
icon: icons[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
|
// 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 startOfHour = DateTime.utc().startOf('hour').toMillis();
|
||||||
const result = []; // resulting expanded values
|
const result = []; // resulting expanded values
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`largeIcon: ${error.message}`);
|
console.warn(`largeIcon: ${error.message}`);
|
||||||
// Return a fallback icon to prevent downstream errors
|
// 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
|
// find the icon
|
||||||
@@ -102,6 +102,8 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'snow_fzra':
|
case 'snow_fzra':
|
||||||
case 'snow_fzra-n':
|
case 'snow_fzra-n':
|
||||||
|
case 'winter_mix':
|
||||||
|
case 'winter_mix-n':
|
||||||
return addPath('Freezing-Rain-Snow.gif');
|
return addPath('Freezing-Rain-Snow.gif');
|
||||||
|
|
||||||
case 'fzra':
|
case 'fzra':
|
||||||
@@ -141,6 +143,8 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
return addPath('Thunderstorm.gif');
|
return addPath('Thunderstorm.gif');
|
||||||
|
|
||||||
case 'wind_skc':
|
case 'wind_skc':
|
||||||
|
case 'wind_':
|
||||||
|
case 'wind_-n':
|
||||||
return addPath('Windy.gif');
|
return addPath('Windy.gif');
|
||||||
|
|
||||||
case 'wind_skc-n':
|
case 'wind_skc-n':
|
||||||
@@ -169,7 +173,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
default: {
|
default: {
|
||||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
// Return a reasonable fallback instead of false to prevent downstream errors
|
// 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':
|
||||||
case 'wind_few-n':
|
case 'wind_few-n':
|
||||||
|
case 'wind_':
|
||||||
return addPath('Wind.gif');
|
return addPath('Wind.gif');
|
||||||
|
|
||||||
case 'wind_sct':
|
case 'wind_sct':
|
||||||
@@ -170,7 +171,7 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'blizzard':
|
case 'blizzard':
|
||||||
case 'blizzard-n':
|
case 'blizzard-n':
|
||||||
return addPath('Blowing Snow.gif');
|
return addPath('Blowing-Snow.gif');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
|||||||
weatherParameters.forecast = point.properties.forecast;
|
weatherParameters.forecast = point.properties.forecast;
|
||||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||||
weatherParameters.stations = stations.features;
|
weatherParameters.stations = stations.features;
|
||||||
|
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
|
||||||
|
|
||||||
// update the main process for display purposes
|
// update the main process for display purposes
|
||||||
populateWeatherParameters(weatherParameters, point.properties);
|
populateWeatherParameters(weatherParameters, point.properties);
|
||||||
@@ -330,6 +331,7 @@ const handleNavButton = (button) => {
|
|||||||
break;
|
break;
|
||||||
case 'menu':
|
case 'menu':
|
||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
|
postMessage({ type: 'current-weather-scroll', method: 'hide' });
|
||||||
if (progress) {
|
if (progress) {
|
||||||
progress.showCanvas();
|
progress.showCanvas();
|
||||||
} else if (settings?.kiosk?.value) {
|
} else if (settings?.kiosk?.value) {
|
||||||
@@ -357,6 +359,17 @@ const isIOS = () => {
|
|||||||
let lastAppliedScale = null;
|
let lastAppliedScale = null;
|
||||||
let lastAppliedKioskMode = null;
|
let lastAppliedKioskMode = null;
|
||||||
|
|
||||||
|
// Helper function to clear CSS properties from elements
|
||||||
|
const clearElementStyles = (element, properties) => {
|
||||||
|
properties.forEach((prop) => element.style.removeProperty(prop));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define property groups for different scaling modes
|
||||||
|
const SCALING_PROPERTIES = {
|
||||||
|
wrapper: ['width', 'height', 'transform', 'transform-origin'],
|
||||||
|
positioning: ['transform', 'transform-origin', 'width', 'height', 'position', 'left', 'top', 'margin-left', 'margin-top'],
|
||||||
|
};
|
||||||
|
|
||||||
// resize the container on a page resize
|
// resize the container on a page resize
|
||||||
const resize = (force = false) => {
|
const resize = (force = false) => {
|
||||||
// Ignore resize events caused by pinch-to-zoom on mobile
|
// Ignore resize events caused by pinch-to-zoom on mobile
|
||||||
@@ -376,9 +389,8 @@ const resize = (force = false) => {
|
|||||||
// Standard scaling: fit within both dimensions
|
// Standard scaling: fit within both dimensions
|
||||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||||
|
|
||||||
// For Mobile Safari in kiosk mode, always use centering behavior regardless of scale
|
// Use centering behavior for fullscreen, kiosk mode, or Mobile Safari kiosk mode
|
||||||
// For other platforms, only use fullscreen/centering behavior for actual fullscreen or kiosk mode where content fits naturally
|
const isKioskLike = isFullscreen || isKioskMode || isMobileSafariKiosk;
|
||||||
const isKioskLike = isFullscreen || (isKioskMode && scale >= 1.0) || isMobileSafariKiosk;
|
|
||||||
|
|
||||||
if (debugFlag('resize') || debugFlag('fullscreen')) {
|
if (debugFlag('resize') || debugFlag('fullscreen')) {
|
||||||
console.log(`🖥️ Resize: force=${force} isKioskLike=${isKioskLike} window=${window.innerWidth}x${window.innerHeight} targetWidth=${targetWidth} widthZoom=${widthZoomPercent.toFixed(3)} heightZoom=${heightZoomPercent.toFixed(3)} finalScale=${scale.toFixed(3)} fullscreenElement=${!!document.fullscreenElement} isIOS=${isIOS()} standalone=${window.navigator.standalone} isMobileSafariKiosk=${isMobileSafariKiosk} kioskMode=${settings.kiosk?.value} wideMode=${settings.wide.value}`);
|
console.log(`🖥️ Resize: force=${force} isKioskLike=${isKioskLike} window=${window.innerWidth}x${window.innerHeight} targetWidth=${targetWidth} widthZoom=${widthZoomPercent.toFixed(3)} heightZoom=${heightZoomPercent.toFixed(3)} finalScale=${scale.toFixed(3)} fullscreenElement=${!!document.fullscreenElement} isIOS=${isIOS()} standalone=${window.navigator.standalone} isMobileSafariKiosk=${isMobileSafariKiosk} kioskMode=${settings.kiosk?.value} wideMode=${settings.wide.value}`);
|
||||||
@@ -412,40 +424,35 @@ const resize = (force = false) => {
|
|||||||
console.log('🖥️ Resetting fullscreen/kiosk styles to normal');
|
console.log('🖥️ Resetting fullscreen/kiosk styles to normal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset wrapper styles (only properties that are actually set in fullscreen/scaling modes)
|
// Reset all scaling-related styles
|
||||||
wrapper.style.removeProperty('width');
|
const container = document.querySelector('#container');
|
||||||
wrapper.style.removeProperty('height');
|
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
|
||||||
wrapper.style.removeProperty('overflow');
|
clearElementStyles(container, SCALING_PROPERTIES.positioning);
|
||||||
wrapper.style.removeProperty('transform');
|
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
|
||||||
wrapper.style.removeProperty('transform-origin');
|
|
||||||
|
|
||||||
// Reset container styles that might have been applied during fullscreen
|
|
||||||
mainContainer.style.removeProperty('transform');
|
|
||||||
mainContainer.style.removeProperty('transform-origin');
|
|
||||||
mainContainer.style.removeProperty('width');
|
|
||||||
mainContainer.style.removeProperty('height');
|
|
||||||
mainContainer.style.removeProperty('position');
|
|
||||||
mainContainer.style.removeProperty('left');
|
|
||||||
mainContainer.style.removeProperty('top');
|
|
||||||
mainContainer.style.removeProperty('margin-left');
|
|
||||||
mainContainer.style.removeProperty('margin-top');
|
|
||||||
|
|
||||||
applyScanlineScaling(1.0);
|
applyScanlineScaling(1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not Mobile Safari kiosk mode)
|
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not when in fullscreen/kiosk mode)
|
||||||
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk) {
|
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk && !isKioskLike) {
|
||||||
/*
|
/*
|
||||||
* MOBILE SCALING (Wrapper Scaling)
|
* MOBILE SCALING (Wrapper Scaling)
|
||||||
*
|
*
|
||||||
|
* This path is used for regular mobile browsing (NOT fullscreen/kiosk modes).
|
||||||
* Why scale the wrapper instead of mainContainer?
|
* Why scale the wrapper instead of mainContainer?
|
||||||
* - For mobile devices where content is larger than viewport, we need to scale the entire layout
|
* - For mobile devices where content is larger than viewport, we need to scale the entire layout
|
||||||
* - The wrapper (#divTwc) contains both the main content AND the bottom navigation bar
|
* - The wrapper (#divTwc) contains both the main content AND the bottom navigation bar
|
||||||
* - Scaling the wrapper ensures both elements are scaled together as a unit
|
* - Scaling the wrapper ensures both elements are scaled together as a unit
|
||||||
* - No centering is applied - content aligns to top-left for typical mobile behavior
|
* - Content aligns to top-left for typical mobile web browsing behavior (no centering)
|
||||||
* - Uses explicit dimensions to prevent layout issues and eliminate gaps after scaling
|
* - Uses explicit dimensions to prevent layout issues and eliminate gaps after scaling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Reset any container/mainContainer styles that might have been set during fullscreen/kiosk mode
|
||||||
|
const container = document.querySelector('#container');
|
||||||
|
clearElementStyles(container, SCALING_PROPERTIES.positioning);
|
||||||
|
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
|
||||||
|
|
||||||
wrapper.style.setProperty('transform', `scale(${scale})`);
|
wrapper.style.setProperty('transform', `scale(${scale})`);
|
||||||
wrapper.style.setProperty('transform-origin', 'top left'); // Scale from top-left corner
|
wrapper.style.setProperty('transform-origin', 'top left'); // Scale from top-left corner
|
||||||
|
|
||||||
@@ -458,7 +465,7 @@ const resize = (force = false) => {
|
|||||||
const scaledHeight = totalHeight * scale; // Height after scaling
|
const scaledHeight = totalHeight * scale; // Height after scaling
|
||||||
|
|
||||||
wrapper.style.setProperty('width', `${wrapperWidth}px`);
|
wrapper.style.setProperty('width', `${wrapperWidth}px`);
|
||||||
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap
|
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap under #divTwc on index page
|
||||||
applyScanlineScaling(scale);
|
applyScanlineScaling(scale);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -468,10 +475,7 @@ const resize = (force = false) => {
|
|||||||
const wrapperHeight = 480;
|
const wrapperHeight = 480;
|
||||||
|
|
||||||
// Reset wrapper styles to avoid double scaling (wrapper remains unstyled)
|
// Reset wrapper styles to avoid double scaling (wrapper remains unstyled)
|
||||||
wrapper.style.removeProperty('width');
|
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
|
||||||
wrapper.style.removeProperty('height');
|
|
||||||
wrapper.style.removeProperty('transform');
|
|
||||||
wrapper.style.removeProperty('transform-origin');
|
|
||||||
|
|
||||||
// Platform-specific positioning logic
|
// Platform-specific positioning logic
|
||||||
let transformOrigin;
|
let transformOrigin;
|
||||||
@@ -529,7 +533,7 @@ const resize = (force = false) => {
|
|||||||
const offsetY = (window.innerHeight - scaledHeight) / 2;
|
const offsetY = (window.innerHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
if (debugFlag('fullscreen')) {
|
if (debugFlag('fullscreen')) {
|
||||||
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} transform: scale(${scale}) translate(${offsetX / scale}px, ${offsetY / scale}px)`);
|
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} target=${isFullscreen ? '#container' : '#divTwcMain'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set positioning values for CSS-based centering
|
// Set positioning values for CSS-based centering
|
||||||
@@ -540,25 +544,41 @@ const resize = (force = false) => {
|
|||||||
marginTop = `-${wrapperHeight / 2}px`; // Pull back by half height
|
marginTop = `-${wrapperHeight / 2}px`; // Pull back by half height
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply shared mainContainer properties (same for both kiosk modes)
|
// Chrome fullscreen compatibility: apply transform to #container instead of #divTwcMain
|
||||||
mainContainer.style.setProperty('transform', `scale(${scale})`, 'important');
|
// This works around Chrome's restriction on styling fullscreen elements directly
|
||||||
mainContainer.style.setProperty('transform-origin', transformOrigin, 'important');
|
const container = document.querySelector('#container');
|
||||||
mainContainer.style.setProperty('width', `${wrapperWidth}px`, 'important');
|
const targetElement = isFullscreen ? container : mainContainer;
|
||||||
mainContainer.style.setProperty('height', `${wrapperHeight}px`, 'important');
|
|
||||||
mainContainer.style.setProperty('position', 'absolute', 'important');
|
// Reset the other element's styles to avoid conflicts
|
||||||
mainContainer.style.setProperty('left', leftPosition, 'important');
|
if (isFullscreen) {
|
||||||
mainContainer.style.setProperty('top', topPosition, 'important');
|
// Reset mainContainer styles when using container for fullscreen
|
||||||
|
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
|
||||||
|
} else {
|
||||||
|
// Reset container styles when using mainContainer for kiosk mode
|
||||||
|
clearElementStyles(container, SCALING_PROPERTIES.positioning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply shared properties to the target element
|
||||||
|
targetElement.style.setProperty('transform', `scale(${scale})`, 'important');
|
||||||
|
targetElement.style.setProperty('transform-origin', transformOrigin, 'important');
|
||||||
|
// the width of the target element does not change it is the fixed width of the 4:3 display which is then scaled
|
||||||
|
// the wrapper adds margins and padding to achieve widescreen
|
||||||
|
// targetElement.style.setProperty('width', `${wrapperWidth}px`, 'important');
|
||||||
|
targetElement.style.setProperty('height', `${wrapperHeight}px`, 'important');
|
||||||
|
targetElement.style.setProperty('position', 'absolute', 'important');
|
||||||
|
targetElement.style.setProperty('left', leftPosition, 'important');
|
||||||
|
targetElement.style.setProperty('top', topPosition, 'important');
|
||||||
|
|
||||||
// Apply or clear margin properties based on positioning method
|
// Apply or clear margin properties based on positioning method
|
||||||
if (marginLeft !== null) {
|
if (marginLeft !== null) {
|
||||||
mainContainer.style.setProperty('margin-left', marginLeft, 'important');
|
targetElement.style.setProperty('margin-left', marginLeft, 'important');
|
||||||
} else {
|
} else {
|
||||||
mainContainer.style.removeProperty('margin-left');
|
targetElement.style.removeProperty('margin-left');
|
||||||
}
|
}
|
||||||
if (marginTop !== null) {
|
if (marginTop !== null) {
|
||||||
mainContainer.style.setProperty('margin-top', marginTop, 'important');
|
targetElement.style.setProperty('margin-top', marginTop, 'important');
|
||||||
} else {
|
} else {
|
||||||
mainContainer.style.removeProperty('margin-top');
|
targetElement.style.removeProperty('margin-top');
|
||||||
}
|
}
|
||||||
|
|
||||||
applyScanlineScaling(scale);
|
applyScanlineScaling(scale);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class Progress extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async drawCanvas(displays, loadedCount) {
|
async drawCanvas(displays, loadedCount) {
|
||||||
|
// skip drawing if not displayed, or not yet available
|
||||||
if (!this.elem) return;
|
if (!this.elem) return;
|
||||||
|
if (this.elem.classList.contains('show') === false) return;
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
|
|
||||||
// get the progress bar cover (makes percentage)
|
// get the progress bar cover (makes percentage)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||||
|
|
||||||
// get a target distance
|
// get a target distance
|
||||||
let targetDistance = 2.5;
|
let targetDistance = 2.4;
|
||||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||||
|
|
||||||
// make station info into an array
|
// make station info into an array
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class SpcOutlook extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
|
if (weatherParameters) this.weatherParameters = weatherParameters;
|
||||||
if (!super.getData(weatherParameters, refresh)) return;
|
if (!super.getData(weatherParameters, refresh)) return;
|
||||||
|
|
||||||
// SPC outlook data does not need to be reloaded on a location change, only during silent refresh
|
// SPC outlook data does not need to be reloaded on a location change, only during silent refresh
|
||||||
@@ -93,7 +94,7 @@ class SpcOutlook extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// parse the data
|
// parse the data
|
||||||
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.rawOutlookData);
|
this.data = testAllPoints([this.weatherParameters.longitude, this.weatherParameters.latitude], this.rawOutlookData);
|
||||||
|
|
||||||
// check if there's a "risk" for any of the three days, otherwise skip the SPC Outlook screen
|
// check if there's a "risk" for any of the three days, otherwise skip the SPC Outlook screen
|
||||||
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {
|
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {
|
||||||
|
|||||||
@@ -3,13 +3,33 @@ import { parseMetar } from '../../vendor/auto/metar-taf-parser.mjs';
|
|||||||
// eslint-disable-next-line import/extensions
|
// eslint-disable-next-line import/extensions
|
||||||
import en from '../../vendor/auto/locale/en.js';
|
import en from '../../vendor/auto/locale/en.js';
|
||||||
|
|
||||||
|
// metar-taf-parser requires regex lookbehind
|
||||||
|
// this does not work in iOS < 16.4
|
||||||
|
// this is a detection algorithm for missing lookbehind support
|
||||||
|
const supportsRegexLookAheadLookBehindCheck = () => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
// deliberately using RegExp for broader browser support during check
|
||||||
|
/* eslint-disable prefer-regex-literals */
|
||||||
|
'hibyehihi'
|
||||||
|
.replace(new RegExp('(?<=hi)hi', 'g'), 'hello')
|
||||||
|
.replace(new RegExp('hi(?!bye)', 'g'), 'hey') === 'hibyeheyhello'
|
||||||
|
/* eslint-enable prefer-regex-literals */
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const supportsRegexLookAheadLookBehind = supportsRegexLookAheadLookBehindCheck();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Augment observation data by parsing METAR when API fields are missing
|
* Augment observation data by parsing METAR when API fields are missing
|
||||||
* @param {Object} observation - The observation object from the API
|
* @param {Object} observation - The observation object from the API
|
||||||
* @returns {Object} - Augmented observation with parsed METAR data filled in
|
* @returns {Object} - Augmented observation with parsed METAR data filled in
|
||||||
*/
|
*/
|
||||||
const augmentObservationWithMetar = (observation) => {
|
const augmentObservationWithMetar = (observation) => {
|
||||||
if (!observation?.rawMessage) {
|
// check for a metar message and for regex lookbehind support
|
||||||
|
if (!observation?.rawMessage || (!supportsRegexLookAheadLookBehind)) {
|
||||||
return observation;
|
return observation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -172,6 +172,7 @@ class WeatherDisplay {
|
|||||||
if (this.screenIndex < 0) this.screenIndex = 0;
|
if (this.screenIndex < 0) this.screenIndex = 0;
|
||||||
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
|
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
|
||||||
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
|
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
|
||||||
|
if (this.okToDrawCurrentConditions === false) postMessage({ type: 'current-weather-scroll', method: 'hide' });
|
||||||
}
|
}
|
||||||
|
|
||||||
finishDraw() {
|
finishDraw() {
|
||||||
|
|||||||
1
server/scripts/vendor/auto/luxon.js.map
vendored
1
server/scripts/vendor/auto/luxon.js.map
vendored
File diff suppressed because one or more lines are too long
4
server/scripts/vendor/auto/luxon.mjs
vendored
4
server/scripts/vendor/auto/luxon.mjs
vendored
@@ -8127,7 +8127,7 @@ function friendlyDateTime(dateTimeish) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "3.7.1";
|
const VERSION = "3.7.2";
|
||||||
|
|
||||||
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||||
//# sourceMappingURL=luxon.js.map
|
//# sourceMappingURL=luxon.mjs.map
|
||||||
|
|||||||
1
server/scripts/vendor/auto/luxon.mjs.map
vendored
Normal file
1
server/scripts/vendor/auto/luxon.mjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,9 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
|
#hazards-html.weather-display {
|
||||||
|
background-image: url('../images/backgrounds/7.png');
|
||||||
|
}
|
||||||
|
|
||||||
.weather-display .main.hazards {
|
.weather-display .main.hazards {
|
||||||
&.main {
|
&.main {
|
||||||
@@ -7,6 +11,7 @@
|
|||||||
height: 480px;
|
height: 480px;
|
||||||
background-color: rgb(112, 35, 35);
|
background-color: rgb(112, 35, 35);
|
||||||
|
|
||||||
|
|
||||||
.hazard-lines {
|
.hazard-lines {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -26,3 +31,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide.hazards #container {
|
||||||
|
background: url(../images/backgrounds/7-wide.png);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
right: 60px;
|
right: 60px;
|
||||||
width: 360px;
|
width: 360px;
|
||||||
font-family: 'Star4000 Small';
|
font-family: 'Star4000 Small';
|
||||||
font-size: 32px;
|
font-size: 28px;
|
||||||
@include u.text-shadow();
|
@include u.text-shadow();
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dewpoint {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
.cloud {
|
.cloud {
|
||||||
color: lightgrey;
|
color: lightgrey;
|
||||||
}
|
}
|
||||||
@@ -52,32 +56,33 @@
|
|||||||
|
|
||||||
.x-axis {
|
.x-axis {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: 0px;
|
left: 54px;
|
||||||
width: 640px;
|
width: 532px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 50px;
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.l-1 {
|
&.l-1 {
|
||||||
left: 25px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-2 {
|
&.l-2 {
|
||||||
left: 158px;
|
left: calc(532px / 4 * 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-3 {
|
&.l-3 {
|
||||||
left: 291px;
|
left: calc(532px / 4 * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-4 {
|
&.l-4 {
|
||||||
left: 424px;
|
left: calc(532px / 4 * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-5 {
|
&.l-5 {
|
||||||
left: 557px;
|
left: calc(532px / 4 * 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +115,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.l-2 {
|
&.l-2 {
|
||||||
top: 140px;
|
top: calc(280px / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-3 {
|
&.l-3 {
|
||||||
|
bottom: calc(280px / 3 - 11px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-4 {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Star4000";
|
font-family: "Star4000";
|
||||||
@@ -161,6 +161,7 @@ body {
|
|||||||
#divTwcMain {
|
#divTwcMain {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
height: 480px;
|
height: 480px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.wide & {
|
.wide & {
|
||||||
width: 854px;
|
width: 854px;
|
||||||
@@ -340,13 +341,14 @@ body {
|
|||||||
// overflow: hidden;
|
// overflow: hidden;
|
||||||
background-image: url(../images/backgrounds/1.png);
|
background-image: url(../images/backgrounds/1.png);
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wide #container {
|
.wide #container {
|
||||||
padding-left: 107px;
|
padding-left: 107px;
|
||||||
padding-right: 107px;
|
padding-right: 107px;
|
||||||
|
background: url(../images/backgrounds/1-wide.png);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background: url(../images/backgrounds/1-wide.png)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwc:fullscreen #container,
|
#divTwc:fullscreen #container,
|
||||||
@@ -814,3 +816,9 @@ body.kiosk #loading .instructions {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#divInfo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
.weather-display {
|
.weather-display {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
@@ -112,29 +112,32 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.scroll {
|
#container>.scroll {
|
||||||
@include u.text-shadow(3px, 1.5px);
|
display: none;
|
||||||
|
@include u.text-shadow(3px, 1.5px);
|
||||||
|
width: 640px;
|
||||||
|
height: 77px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 3px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&.hazard {
|
||||||
|
background-color: rgb(112, 35, 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
height: 70px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 3px;
|
|
||||||
|
|
||||||
&.hazard {
|
|
||||||
background-color: rgb(112, 35, 35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed,
|
.fixed,
|
||||||
.scroll-header {
|
.scroll-header {
|
||||||
margin-left: 55px;
|
margin-left: 55px;
|
||||||
margin-right: 55px;
|
margin-right: 55px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
white-space: nowrap;
|
||||||
|
|
||||||
// Remove margins for hazard scrolls to maximize text space
|
|
||||||
&.hazard .fixed {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-header {
|
.scroll-header {
|
||||||
@@ -156,6 +159,17 @@
|
|||||||
// left: calc((elem width) - 640px);
|
// left: calc((elem width) - 640px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide #container>.scroll {
|
||||||
|
width: 854px;
|
||||||
|
margin-left: -107px;
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
margin-left: 107px;
|
||||||
|
}
|
||||||
|
}
|
||||||
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
25
src/com.chrome.devtools.mjs
Normal file
25
src/com.chrome.devtools.mjs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// get values for devtools json
|
||||||
|
const uuid = 'd2bd1130-560f-4c8e-b2c5-e91073784964';
|
||||||
|
const root = path.resolve('server');
|
||||||
|
|
||||||
|
const DEVTOOLS_CONFIG = {
|
||||||
|
workspace: {
|
||||||
|
uuid,
|
||||||
|
root,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const devTools = (req, res) => {
|
||||||
|
// test for localhost
|
||||||
|
if (['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.ip)) {
|
||||||
|
console.log(DEVTOOLS_CONFIG);
|
||||||
|
res.json(DEVTOOLS_CONFIG);
|
||||||
|
} else {
|
||||||
|
// not localhost
|
||||||
|
res.status(404).send('File not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default devTools;
|
||||||
@@ -12,6 +12,9 @@ url_encode() {
|
|||||||
|
|
||||||
# build query string from WSQS_ env vars
|
# build query string from WSQS_ env vars
|
||||||
while IFS='=' read -r key val; do
|
while IFS='=' read -r key val; do
|
||||||
|
# Skip empty lines
|
||||||
|
[ -z "$key" ] && continue
|
||||||
|
|
||||||
# Remove WSQS_ prefix and convert underscores to hyphens
|
# Remove WSQS_ prefix and convert underscores to hyphens
|
||||||
key="${key#WSQS_}"
|
key="${key#WSQS_}"
|
||||||
key="${key//_/-}"
|
key="${key//_/-}"
|
||||||
@@ -23,11 +26,16 @@ while IFS='=' read -r key val; do
|
|||||||
QS="${key}=${encoded_val}"
|
QS="${key}=${encoded_val}"
|
||||||
fi
|
fi
|
||||||
done << EOF
|
done << EOF
|
||||||
$(env | grep '^WSQS_')
|
$(env | grep '^WSQS_' || true)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
mkdir -p /etc/nginx/includes
|
||||||
|
|
||||||
if [ -n "$QS" ]; then
|
if [ -n "$QS" ]; then
|
||||||
|
# Escape the query string for use in JavaScript (escape backslashes and single quotes)
|
||||||
|
QS_ESCAPED=$(printf '%s' "$QS" | sed "s/\\\\/\\\\\\\\/g; s/'/\\\'/g")
|
||||||
|
|
||||||
|
# Generate redirect.html with JavaScript logic
|
||||||
cat > "$ROOT/redirect.html" <<EOF
|
cat > "$ROOT/redirect.html" <<EOF
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -35,10 +43,36 @@ if [ -n "$QS" ]; then
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Redirecting</title>
|
<title>Redirecting</title>
|
||||||
<meta http-equiv="refresh" content="0;url=/index.html?$QS" />
|
<meta http-equiv="refresh" content="0;url=/index.html?$QS" />
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var wsqsParams = '$QS_ESCAPED';
|
||||||
|
var currentParams = window.location.search.substring(1);
|
||||||
|
var targetParams = currentParams || wsqsParams;
|
||||||
|
window.location.replace('/index.html?' + targetParams);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Generate nginx config for conditional redirects
|
||||||
|
cat > /etc/nginx/includes/wsqs_redirect.conf <<'EOF'
|
||||||
|
location = / {
|
||||||
|
if ($args = '') {
|
||||||
|
rewrite ^ /redirect.html last;
|
||||||
|
}
|
||||||
|
rewrite ^/$ /index.html?$args? redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
if ($args = '') {
|
||||||
|
rewrite ^ /redirect.html last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
touch /etc/nginx/includes/wsqs_redirect.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec nginx -g 'daemon off;'
|
exec nginx -g 'daemon off;'
|
||||||
|
|||||||
268
tests/package-lock.json
generated
268
tests/package-lock.json
generated
@@ -28,26 +28,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@puppeteer/browsers": {
|
"node_modules/@puppeteer/browsers": {
|
||||||
"version": "2.10.5",
|
"version": "2.10.13",
|
||||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz",
|
||||||
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
"integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.3",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.3",
|
||||||
"tar-fs": "^3.0.8",
|
"tar-fs": "^3.1.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -64,13 +64,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.29",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
@@ -84,9 +84,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
@@ -135,28 +135,45 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||||
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
|
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-events": {
|
"node_modules/bare-events": {
|
||||||
"version": "2.5.4",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-fs": {
|
"node_modules/bare-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.1.tgz",
|
||||||
"integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
|
"integrity": "sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-events": "^2.5.4",
|
"bare-events": "^2.5.4",
|
||||||
"bare-path": "^3.0.0",
|
"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": {
|
"engines": {
|
||||||
"bare": ">=1.16.0"
|
"bare": ">=1.16.0"
|
||||||
@@ -171,9 +188,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-os": {
|
"node_modules/bare-os": {
|
||||||
"version": "3.6.1",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||||
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
|
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -191,9 +208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-stream": {
|
"node_modules/bare-stream": {
|
||||||
"version": "2.6.5",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||||
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
|
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"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": {
|
"node_modules/basic-ftp": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||||
@@ -240,9 +267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "5.4.1",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
@@ -252,9 +279,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromium-bidi": {
|
"node_modules/chromium-bidi": {
|
||||||
"version": "5.1.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-11.0.0.tgz",
|
||||||
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
|
"integrity": "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
@@ -332,9 +359,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -363,10 +390,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1452169",
|
"version": "0.0.1521046",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||||
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
|
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -375,9 +403,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
@@ -393,9 +421,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-arrayish": "^0.2.1"
|
"is-arrayish": "^0.2.1"
|
||||||
@@ -462,6 +490,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -522,9 +559,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-uri": {
|
"node_modules/get-uri": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
|
||||||
"integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
|
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"basic-ftp": "^5.0.2",
|
"basic-ftp": "^5.0.2",
|
||||||
@@ -578,14 +615,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "9.0.5",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"jsbn": "1.1.0",
|
|
||||||
"sprintf-js": "^1.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
@@ -612,9 +645,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -623,12 +656,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"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": {
|
"node_modules/json-parse-even-better-errors": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
"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"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -799,17 +826,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/puppeteer": {
|
"node_modules/puppeteer": {
|
||||||
"version": "24.10.0",
|
"version": "24.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.31.0.tgz",
|
||||||
"integrity": "sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==",
|
"integrity": "sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "2.10.5",
|
"@puppeteer/browsers": "2.10.13",
|
||||||
"chromium-bidi": "5.1.0",
|
"chromium-bidi": "11.0.0",
|
||||||
"cosmiconfig": "^9.0.0",
|
"cosmiconfig": "^9.0.0",
|
||||||
"devtools-protocol": "0.0.1452169",
|
"devtools-protocol": "0.0.1521046",
|
||||||
"puppeteer-core": "24.10.0",
|
"puppeteer-core": "24.31.0",
|
||||||
"typed-query-selector": "^2.12.0"
|
"typed-query-selector": "^2.12.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -820,17 +847,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/puppeteer-core": {
|
"node_modules/puppeteer-core": {
|
||||||
"version": "24.10.0",
|
"version": "24.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.31.0.tgz",
|
||||||
"integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==",
|
"integrity": "sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "2.10.5",
|
"@puppeteer/browsers": "2.10.13",
|
||||||
"chromium-bidi": "5.1.0",
|
"chromium-bidi": "11.0.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.3",
|
||||||
"devtools-protocol": "0.0.1452169",
|
"devtools-protocol": "0.0.1521046",
|
||||||
"typed-query-selector": "^2.12.0",
|
"typed-query-selector": "^2.12.0",
|
||||||
"ws": "^8.18.2"
|
"webdriver-bidi-protocol": "0.3.9",
|
||||||
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -855,9 +883,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -877,12 +905,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socks": {
|
"node_modules/socks": {
|
||||||
"version": "2.8.4",
|
"version": "2.8.7",
|
||||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "^9.0.5",
|
"ip-address": "^10.0.1",
|
||||||
"smart-buffer": "^4.2.0"
|
"smart-buffer": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -914,23 +942,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/streamx": {
|
||||||
"version": "2.22.0",
|
"version": "2.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||||
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
"fast-fifo": "^1.3.2",
|
"fast-fifo": "^1.3.2",
|
||||||
"text-decoder": "^1.1.0"
|
"text-decoder": "^1.1.0"
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"bare-events": "^2.2.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
@@ -960,9 +980,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "3.0.9",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||||
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
@@ -1006,12 +1026,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@@ -1036,9 +1062,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.2",
|
"version": "8.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -1103,9 +1129,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.49",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
const OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
const OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||||
</script>
|
</script>
|
||||||
<% } else { %>
|
<% } 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">const OVERRIDES={};</script>-->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||||
@@ -133,6 +133,7 @@
|
|||||||
<div id="hazards-html" class="weather-display">
|
<div id="hazards-html" class="weather-display">
|
||||||
<%- include('partials/hazards.ejs') %>
|
<%- include('partials/hazards.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
|
<%- include('partials/scroll.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="divTwcBottom">
|
<div id="divTwcBottom">
|
||||||
@@ -146,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" />
|
||||||
@@ -185,16 +192,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='heading'>Forecast Information</div>
|
<div class='heading'>Headend Information</div>
|
||||||
<div id="divInfo">
|
<div id="divInfo">
|
||||||
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
|
<div class="header">Location:</div>
|
||||||
Station Id: <span id="spanStationId"></span><br />
|
<div class="header"><span id="spanCity"></span> <span id="spanState"></span></div>
|
||||||
Radar Id: <span id="spanRadarId"></span><br />
|
<div class="header">Station Id:</div>
|
||||||
Zone Id: <span id="spanZoneId"></span><br />
|
<div class="header"><span id="spanStationId"></span></div>
|
||||||
Office Id: <span id="spanOfficeId"></span><br />
|
<div class="header">Radar Id:</div>
|
||||||
Grid X,Y: <span id="spanGridPoint"></span><br />
|
<div class="header"><span id="spanRadarId"></span></div>
|
||||||
Music: <span id="musicTrack">Not playing</span><br />
|
<div class="header">Zone Id:</div>
|
||||||
Ws4kp Version: <span><%- version %></span>
|
<div class="header"><span id="spanZoneId"></span></div>
|
||||||
|
<div class="header">Office Id:</div>
|
||||||
|
<div class="header"><span id="spanOfficeId"></span></div>
|
||||||
|
<div class="header">Grid X,Y:</div>
|
||||||
|
<div class="header"><span id="spanGridPoint"></span></div>
|
||||||
|
<div class="header">Music:</div>
|
||||||
|
<div class="header"><span id="musicTrack">Not playing</span></div>
|
||||||
|
<div class="header">Ws4kp Version:</div>
|
||||||
|
<div class="header"><span><%- version %></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,5 +21,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -1,43 +1,42 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
||||||
<div class="main has-scroll has-box current-weather">
|
<div class="main has-scroll has-box current-weather">
|
||||||
<div class="weather template">
|
<div class="weather template">
|
||||||
<div class="left col">
|
<div class="left col">
|
||||||
<div class="temp center"></div>
|
<div class="temp center"></div>
|
||||||
<div class="condition center"></div>
|
<div class="condition center"></div>
|
||||||
<div class="icon center"><img src="" /></div>
|
<div class="icon center"><img src="" /></div>
|
||||||
<div class="wind-container">
|
<div class="wind-container">
|
||||||
<div class="wind-label">Wind:</div>
|
<div class="wind-label">Wind:</div>
|
||||||
<div class="wind"></div>
|
<div class="wind"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wind-gusts"></div>
|
<div class="wind-gusts"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right col">
|
<div class="right col">
|
||||||
<div class="location"></div>
|
<div class="location"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Humidity:</div>
|
<div class="label">Humidity:</div>
|
||||||
<div class="humidity value"></div>
|
<div class="humidity value"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Dewpoint:</div>
|
<div class="label">Dewpoint:</div>
|
||||||
<div class="dewpoint value"></div>
|
<div class="dewpoint value"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Ceiling:</div>
|
<div class="label">Ceiling:</div>
|
||||||
<div class="ceiling value"></div>
|
<div class="ceiling value"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Visibility:</div>
|
<div class="label">Visibility:</div>
|
||||||
<div class="visibility value"></div>
|
<div class="visibility value"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Pressure:</div>
|
<div class="label">Pressure:</div>
|
||||||
<div class="pressure value"></div>
|
<div class="pressure value"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="heat-index-label label"></div>
|
<div class="heat-index-label label"></div>
|
||||||
<div class="heat-index value"></div>
|
<div class="heat-index value"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -19,5 +19,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
|
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
|
||||||
<div class="main has-scroll hourly-graph">
|
<div class="main has-scroll hourly-graph">
|
||||||
<div class="top-right template ">
|
<div class="top-right template ">
|
||||||
<div class="temperature">Temperature</div>
|
<div class="temperature">Temperature</div>
|
||||||
<div class="cloud">Cloud %</div>
|
<div class="dewpoint">Dewpoint</div>
|
||||||
<div class="rain">Precip %</div>
|
<div class="cloud">Cloud %</div>
|
||||||
</div>
|
<div class="rain">Precip %</div>
|
||||||
<div class="y-axis">
|
</div>
|
||||||
<div class="label l-1">75</div>
|
<div class="y-axis">
|
||||||
<div class="label l-2">65</div>
|
<div class="label l-1">75</div>
|
||||||
<div class="label l-3">55</div>
|
<div class="label l-2">65</div>
|
||||||
</div>
|
<div class="label l-3">55</div>
|
||||||
<div class="chart">
|
<div class="label l-4">45</div>
|
||||||
<img id="chart-area"></img>
|
</div>
|
||||||
</div>
|
<div class="chart">
|
||||||
<div class="x-axis">
|
<img id="chart-area"></img>
|
||||||
<div class="label l-1">12a</div>
|
</div>
|
||||||
<div class="label l-2">6a</div>
|
<div class="x-axis">
|
||||||
<div class="label l-3">12p</div>
|
<div class="label l-1">12a</div>
|
||||||
<div class="label l-4">6p</div>
|
<div class="label l-2">6a</div>
|
||||||
<div class="label l-5">12a</div>
|
<div class="label l-3">12p</div>
|
||||||
</div>
|
<div class="label l-4">6p</div>
|
||||||
</div>
|
<div class="label l-5">12a</div>
|
||||||
<%- include('scroll.ejs') %>
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
<%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %>
|
<%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %>
|
||||||
<div class="main has-scroll hourly">
|
<div class="main has-scroll hourly">
|
||||||
<div class="column-headers">
|
<div class="column-headers">
|
||||||
<div class="temp">TEMP</div>
|
<div class="temp">TEMP</div>
|
||||||
<div class="like">LIKE</div>
|
<div class="like">LIKE</div>
|
||||||
<div class="wind">WIND</div>
|
<div class="wind">WIND</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hourly-lines">
|
<div class="hourly-lines">
|
||||||
<div class="hourly-row template">
|
<div class="hourly-row template">
|
||||||
<div class="hour"></div>
|
<div class="hour"></div>
|
||||||
<div class="icon"><img /></div>
|
<div class="icon"><img /></div>
|
||||||
<div class="temp"></div>
|
<div class="temp"></div>
|
||||||
<div class="like"></div>
|
<div class="like"></div>
|
||||||
<div class="wind"></div>
|
<div class="wind"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
||||||
<div class="main has-scroll latest-observations has-box">
|
<div class="main has-scroll latest-observations has-box">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="column-headers">
|
<div class="column-headers">
|
||||||
<div class="temp english">°F</div>
|
<div class="temp english">°F</div>
|
||||||
<div class="temp metric">°C</div>
|
<div class="temp metric">°C</div>
|
||||||
<div class="weather">Weather</div>
|
<div class="weather">Weather</div>
|
||||||
<div class="wind">Wind</div>
|
<div class="wind">Wind</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="observation-lines">
|
<div class="observation-lines">
|
||||||
<div class="observation-row template">
|
<div class="observation-row template">
|
||||||
<div class="location"></div>
|
<div class="location"></div>
|
||||||
<div class="temp"></div>
|
<div class="temp"></div>
|
||||||
<div class="weather"></div>
|
<div class="weather"></div>
|
||||||
<div class="wind"></div>
|
<div class="wind"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
||||||
<div class="main has-scroll regional-forecast">
|
<div class="main has-scroll regional-forecast">
|
||||||
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
||||||
<div class="location-container">
|
<div class="location-container">
|
||||||
<div class="location template">
|
<div class="location template">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img src="" />
|
<img src="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="city"></div>
|
<div class="city"></div>
|
||||||
<div class="temp"></div>
|
<div class="temp"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="scrolling template"></div>
|
<div class="scroll-container">
|
||||||
<div class="scroll-header"></div>
|
<div class="scroll-header"></div>
|
||||||
<div class="fixed"></div>
|
<div class="fixed"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: true}) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: true}) %>
|
||||||
<div class="main has-scroll spc-outlook">
|
<div class="main has-scroll spc-outlook">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="risk-levels">
|
<div class="risk-levels">
|
||||||
<div class="risk-level">High</div>
|
<div class="risk-level">High</div>
|
||||||
<div class="risk-level">Moderate</div>
|
<div class="risk-level">Moderate</div>
|
||||||
<div class="risk-level">Enhanced</div>
|
<div class="risk-level">Enhanced</div>
|
||||||
<div class="risk-level">Slight</div>
|
<div class="risk-level">Slight</div>
|
||||||
<div class="risk-level">Marginal</div>
|
<div class="risk-level">Marginal</div>
|
||||||
<div class="risk-level">T'Storm</div>
|
<div class="risk-level">T'Storm</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="days">
|
<div class="days">
|
||||||
<div class="day template">
|
<div class="day template">
|
||||||
<div class="day-name">Monday</div>
|
<div class="day-name">Monday</div>
|
||||||
<div class="risk-bar"></div>
|
<div class="risk-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -12,5 +12,4 @@
|
|||||||
<div class="temp high"></div>
|
<div class="temp high"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -45,7 +45,8 @@
|
|||||||
"unmuted",
|
"unmuted",
|
||||||
"dumpio",
|
"dumpio",
|
||||||
"mesonet",
|
"mesonet",
|
||||||
"metar"
|
"metar",
|
||||||
|
"Unmute"
|
||||||
],
|
],
|
||||||
"cSpell.ignorePaths": [
|
"cSpell.ignorePaths": [
|
||||||
"**/package-lock.json",
|
"**/package-lock.json",
|
||||||
@@ -73,7 +74,10 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
}
|
},
|
||||||
|
"cSpell.words": [
|
||||||
|
"hibyehihi"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
|
|||||||
Reference in New Issue
Block a user