mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 17:19:30 -07:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d810b429c5 | ||
|
|
560b51ccee | ||
|
|
e8f69ce28b | ||
|
|
46fb40058f | ||
|
|
b40fe08465 | ||
|
|
b253988bd3 | ||
|
|
92690e8e97 | ||
|
|
020f0eabb2 | ||
|
|
b812c7b25c | ||
|
|
707144cabd | ||
|
|
372cb0cfab | ||
|
|
64215a117c | ||
|
|
2f0204a689 | ||
|
|
e9164d8c36 | ||
|
|
65444978b7 | ||
|
|
2b4aad3c48 | ||
|
|
1a7d0759c4 | ||
|
|
f0600d92ed | ||
|
|
06b8dbc959 | ||
|
|
dc9a08bdc5 | ||
|
|
0dac24f77d | ||
|
|
c2f0b9bf3f | ||
|
|
cab2da5e62 | ||
|
|
471d322cde | ||
|
|
8f9be046ac | ||
|
|
c34dc1ff25 | ||
|
|
9b4eed7332 | ||
|
|
ef1477f9eb | ||
|
|
e2876df177 | ||
|
|
d6335b2878 | ||
|
|
781128100e | ||
|
|
56261ded4b | ||
|
|
58540ad67b | ||
|
|
af53cca45e | ||
|
|
94470db9a7 | ||
|
|
3c7a77e200 | ||
|
|
d472df2e26 | ||
|
|
fdbf11dcd4 | ||
|
|
b7e9091320 | ||
|
|
d24284d340 | ||
|
|
4e8dc35739 |
@@ -1,3 +1,4 @@
|
|||||||
.git/
|
.git/
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.vscode/
|
.vscode/
|
||||||
|
dist/
|
||||||
91
.eslintrc
Normal file
91
.eslintrc
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true,
|
||||||
|
"jquery": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"airbnb-base"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"TravelCities": "readonly",
|
||||||
|
"RegionalCities": "readonly",
|
||||||
|
"StationInfo": "readonly",
|
||||||
|
"SunCalc": "readonly"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2023,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
"tab",
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-tabs": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"max-len": 0,
|
||||||
|
"no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"variables": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-param-reassign": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"props": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-mixed-operators": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
[
|
||||||
|
"&",
|
||||||
|
"|",
|
||||||
|
"^",
|
||||||
|
"~",
|
||||||
|
"<<",
|
||||||
|
">>",
|
||||||
|
">>>"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
"!=",
|
||||||
|
"===",
|
||||||
|
"!==",
|
||||||
|
">",
|
||||||
|
">=",
|
||||||
|
"<",
|
||||||
|
"<="
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"&&",
|
||||||
|
"||"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"in",
|
||||||
|
"instanceof"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"allowSamePrecedence": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"mjs": "always",
|
||||||
|
"json": "always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"*.min.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
74
.eslintrc.js
74
.eslintrc.js
@@ -1,74 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
node: true,
|
|
||||||
jquery: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'airbnb-base',
|
|
||||||
],
|
|
||||||
globals: {
|
|
||||||
Atomics: 'readonly',
|
|
||||||
SharedArrayBuffer: 'readonly',
|
|
||||||
StationInfo: 'readonly',
|
|
||||||
RegionalCities: 'readonly',
|
|
||||||
TravelCities: 'readonly',
|
|
||||||
NoSleep: 'readonly',
|
|
||||||
states: 'readonly',
|
|
||||||
SunCalc: 'readonly',
|
|
||||||
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2023,
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
indent: [
|
|
||||||
'error',
|
|
||||||
'tab',
|
|
||||||
{
|
|
||||||
SwitchCase: 1
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-tabs': 0,
|
|
||||||
'no-console': 0,
|
|
||||||
'max-len': 0,
|
|
||||||
'no-use-before-define': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
variables: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-param-reassign': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
props: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-mixed-operators': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
groups: [
|
|
||||||
['&', '|', '^', '~', '<<', '>>', '>>>'],
|
|
||||||
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
|
|
||||||
['&&', '||'],
|
|
||||||
['in', 'instanceof'],
|
|
||||||
],
|
|
||||||
allowSamePrecedence: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/extensions': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
mjs: 'always',
|
|
||||||
json: 'always',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ignorePatterns: [
|
|
||||||
'*.min.js',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,3 +1,14 @@
|
|||||||
node_modules
|
node_modules
|
||||||
**/debug.log
|
**/debug.log
|
||||||
server/scripts/custom.js
|
server/scripts/custom.js
|
||||||
|
|
||||||
|
#music folder
|
||||||
|
server/music/*
|
||||||
|
#except for the readme
|
||||||
|
!server/music/readme.txt
|
||||||
|
#and the sample songs
|
||||||
|
!server/music/default
|
||||||
|
|
||||||
|
#dist folder
|
||||||
|
dist/*
|
||||||
|
!dist/readme.txt
|
||||||
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -50,7 +50,7 @@
|
|||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**",
|
"<node_internals>/**",
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/index.js",
|
"program": "${workspaceFolder}/index.mjs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**",
|
"<node_internals>/**",
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/index.js",
|
"program": "${workspaceFolder}/index.mjs",
|
||||||
"env": {
|
"env": {
|
||||||
"DIST": "1"
|
"DIST": "1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ COPY package-lock.json .
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.mjs"]
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -16,6 +16,13 @@ This project is based on the work of [Mike Battaglia](https://github.com/vbguyny
|
|||||||
* [Icon](https://twcclassics.com/downloads.html) sets
|
* [Icon](https://twcclassics.com/downloads.html) sets
|
||||||
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
||||||
|
|
||||||
|
## 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 exclsuive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
||||||
|
|
||||||
|
If you would like to display weather information for international locations (outside of the USA), please checkout a fork of this project created by [@mwood77](https://github.com/mwood77):
|
||||||
|
- [`ws4kp-international`](https://github.com/mwood77/ws4kp-international)
|
||||||
|
|
||||||
## Run Your WeatherStar
|
## Run Your WeatherStar
|
||||||
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
||||||
|
|
||||||
@@ -91,11 +98,37 @@ As time allows I will be working on the following enhancements.
|
|||||||
|
|
||||||
* Better error reporting when api.weather.gov is down (happens more often than you would think)
|
* Better error reporting when api.weather.gov is down (happens more often than you would think)
|
||||||
|
|
||||||
|
## Serving static files
|
||||||
|
The app can be served as a static set of files on any web server. Run the provided gulp task to create a set of static distribution files:
|
||||||
|
```
|
||||||
|
npm run buildDist
|
||||||
|
```
|
||||||
|
The resulting files will be in the /dist folder in the root of the project. These can then be uploaded to a web server for hosting, no server-side scripting is required.
|
||||||
|
|
||||||
|
## Music
|
||||||
|
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
||||||
|
|
||||||
|
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks will be posted in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||||
|
|
||||||
|
### Customizing the music
|
||||||
|
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
||||||
|
|
||||||
|
If using docker, you must pass a local accessible folder to the container in the `/app/server/music` directory.
|
||||||
|
```
|
||||||
|
docker run -p 8080:8080 -v /path/to/local/music:/app/server/music ghcr.io/netbymatt/ws4kp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Music doesn't auto play
|
||||||
|
Ws4kp is muted by default, but if it was unmuted on the last visit it is coded to try and auto play music on subsequent visits. But, it's considered bad form to have a web site play music automatically on load, and I fully agree with this. [Chrome](https://developer.chrome.com/blog/autoplay/#media_engagement_index) and [Firefox](https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/) have extensive details on how and when auto play is allowed.
|
||||||
|
|
||||||
|
Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
||||||
@@ -104,6 +137,8 @@ A hook is provided as `/server/scripts/custom.js` to allow customizations to you
|
|||||||
|
|
||||||
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
||||||
|
|
||||||
|
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// pass through api requests
|
// pass through api requests
|
||||||
|
|
||||||
// http(s) modules
|
// http(s) modules
|
||||||
const https = require('https');
|
import https from 'https';
|
||||||
|
|
||||||
// url parsing
|
// url parsing
|
||||||
const queryString = require('querystring');
|
import queryString from 'querystring';
|
||||||
|
|
||||||
// return an express router
|
// return an express router
|
||||||
module.exports = (req, res) => {
|
const cors = (req, res) => {
|
||||||
// add out-going headers
|
// add out-going headers
|
||||||
const headers = {};
|
const headers = {};
|
||||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||||
@@ -41,3 +41,5 @@ module.exports = (req, res) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default cors;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// pass through api requests
|
// pass through api requests
|
||||||
|
|
||||||
// http(s) modules
|
// http(s) modules
|
||||||
const https = require('https');
|
import https from 'https';
|
||||||
|
|
||||||
// url parsing
|
// url parsing
|
||||||
const queryString = require('querystring');
|
import queryString from 'querystring';
|
||||||
|
|
||||||
// return an express router
|
// return an express router
|
||||||
module.exports = (req, res) => {
|
const outlook = (req, res) => {
|
||||||
// add out-going headers
|
// add out-going headers
|
||||||
const headers = {};
|
const headers = {};
|
||||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||||
@@ -42,3 +42,5 @@ module.exports = (req, res) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default outlook;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// pass through api requests
|
// pass through api requests
|
||||||
|
|
||||||
// http(s) modules
|
// http(s) modules
|
||||||
const https = require('https');
|
import https from 'https';
|
||||||
|
|
||||||
// url parsing
|
// url parsing
|
||||||
const queryString = require('querystring');
|
import queryString from 'querystring';
|
||||||
|
|
||||||
// return an express router
|
// return an express router
|
||||||
module.exports = (req, res) => {
|
const radar = (req, res) => {
|
||||||
// add out-going headers
|
// add out-going headers
|
||||||
const headers = {};
|
const headers = {};
|
||||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||||
@@ -42,3 +42,5 @@ module.exports = (req, res) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default radar;
|
||||||
1
dist/index.html
vendored
1
dist/index.html
vendored
File diff suppressed because one or more lines are too long
12
dist/manifest.json
vendored
12
dist/manifest.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "WeatherStar 4000+",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/images/Logo192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "images/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
1
dist/readme.txt
vendored
Normal file
1
dist/readme.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This folder is a placeholder for static files generated by the gulp task buildDist
|
||||||
1
dist/resources/data.min.js
vendored
1
dist/resources/data.min.js
vendored
File diff suppressed because one or more lines are too long
22
dist/resources/vendor.min.js
vendored
22
dist/resources/vendor.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/resources/ws.min.css
vendored
1
dist/resources/ws.min.css
vendored
File diff suppressed because one or more lines are too long
1
dist/resources/ws.min.js
vendored
1
dist/resources/ws.min.js
vendored
File diff suppressed because one or more lines are too long
0
dist/robots.txt
vendored
0
dist/robots.txt
vendored
@@ -12,11 +12,13 @@ import s3Upload from 'gulp-s3-upload';
|
|||||||
import webpack from 'webpack-stream';
|
import webpack from 'webpack-stream';
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
import file from 'gulp-file';
|
||||||
|
|
||||||
// get cloudfront
|
// get cloudfront
|
||||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||||
|
import reader from '../src/playlist-reader.mjs';
|
||||||
|
|
||||||
const clean = () => deleteAsync(['./dist**']);
|
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||||
|
|
||||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ const mjsSources = [
|
|||||||
'server/scripts/modules/regionalforecast.mjs',
|
'server/scripts/modules/regionalforecast.mjs',
|
||||||
'server/scripts/modules/travelforecast.mjs',
|
'server/scripts/modules/travelforecast.mjs',
|
||||||
'server/scripts/modules/progress.mjs',
|
'server/scripts/modules/progress.mjs',
|
||||||
|
'server/scripts/modules/media.mjs',
|
||||||
'server/scripts/index.mjs',
|
'server/scripts/index.mjs',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -121,8 +124,9 @@ const compressHtml = async () => {
|
|||||||
const otherFiles = [
|
const otherFiles = [
|
||||||
'server/robots.txt',
|
'server/robots.txt',
|
||||||
'server/manifest.json',
|
'server/manifest.json',
|
||||||
|
'server/music/**/*.mp3',
|
||||||
];
|
];
|
||||||
const copyOtherFiles = () => src(otherFiles, { base: 'server/' })
|
const copyOtherFiles = () => src(otherFiles, { base: 'server/', encoding: false })
|
||||||
.pipe(dest('./dist'));
|
.pipe(dest('./dist'));
|
||||||
|
|
||||||
const s3 = s3Upload({
|
const s3 = s3Upload({
|
||||||
@@ -134,13 +138,14 @@ const uploadSources = [
|
|||||||
'dist/**',
|
'dist/**',
|
||||||
'!dist/**/*.map',
|
'!dist/**/*.map',
|
||||||
];
|
];
|
||||||
const upload = () => src(uploadSources, { base: './dist' })
|
const upload = () => src(uploadSources, { base: './dist', encoding: false })
|
||||||
.pipe(s3({
|
.pipe(s3({
|
||||||
Bucket: 'weatherstar',
|
Bucket: 'weatherstar',
|
||||||
StorageClass: 'STANDARD',
|
StorageClass: 'STANDARD',
|
||||||
maps: {
|
maps: {
|
||||||
CacheControl: (keyname) => {
|
CacheControl: (keyname) => {
|
||||||
if (keyname.indexOf('index.html') > -1) return 'max-age=300'; // 10 minutes
|
if (keyname.indexOf('index.html') > -1) return 'max-age=300'; // 10 minutes
|
||||||
|
if (keyname.indexOf('.mp3') > -1) return 'max-age=31536000'; // 1 year for mp3 files
|
||||||
return 'max-age=2592000'; // 1 month
|
return 'max-age=2592000'; // 1 month
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -169,10 +174,20 @@ const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles));
|
const buildPlaylist = async () => {
|
||||||
|
const availableFiles = await reader();
|
||||||
|
const playlist = { availableFiles };
|
||||||
|
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, 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);
|
||||||
|
|
||||||
export default publishFrontend;
|
export default publishFrontend;
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildDist,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import updateVendor from './gulp/update-vendor.mjs';
|
import updateVendor from './gulp/update-vendor.mjs';
|
||||||
import publishFrontend from './gulp/publish-frontend.mjs';
|
import publishFrontend, { buildDist } from './gulp/publish-frontend.mjs'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
updateVendor,
|
updateVendor,
|
||||||
publishFrontend,
|
publishFrontend,
|
||||||
|
buildDist,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
// express
|
import express from 'express';
|
||||||
const express = require('express');
|
import fs from 'fs';
|
||||||
|
import corsPassThru from './cors/index.mjs';
|
||||||
|
import radarPassThru from './cors/radar.mjs';
|
||||||
|
import outlookPassThru from './cors/outlook.mjs';
|
||||||
|
import playlist from './src/playlist.mjs';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.WS4KP_PORT ?? 8080;
|
const port = process.env.WS4KP_PORT ?? 8080;
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// template engine
|
// template engine
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
// cors pass through
|
|
||||||
const fs = require('fs');
|
|
||||||
const corsPassThru = require('./cors');
|
|
||||||
const radarPassThru = require('./cors/radar');
|
|
||||||
const outlookPassThru = require('./cors/outlook');
|
|
||||||
|
|
||||||
// cors pass-thru to api.weather.gov
|
// cors pass-thru to api.weather.gov
|
||||||
app.get('/stations/*', corsPassThru);
|
app.get('/stations/*', corsPassThru);
|
||||||
app.get('/Conus/*', radarPassThru);
|
app.get('/Conus/*', radarPassThru);
|
||||||
app.get('/products/*', outlookPassThru);
|
app.get('/products/*', outlookPassThru);
|
||||||
|
app.get('/playlist.json', playlist);
|
||||||
|
|
||||||
// version
|
// version
|
||||||
const { version } = JSON.parse(fs.readFileSync('package.json'));
|
const { version } = JSON.parse(fs.readFileSync('package.json'));
|
||||||
|
|
||||||
const index = (req, res) => {
|
const index = (req, res) => {
|
||||||
res.render(path.join(__dirname, 'views/index'), {
|
res.render('index', {
|
||||||
production: false,
|
production: false,
|
||||||
version,
|
version,
|
||||||
});
|
});
|
||||||
@@ -32,15 +30,15 @@ const index = (req, res) => {
|
|||||||
// debugging
|
// debugging
|
||||||
if (process.env?.DIST === '1') {
|
if (process.env?.DIST === '1') {
|
||||||
// distribution
|
// distribution
|
||||||
app.use('/images', express.static(path.join(__dirname, './server/images')));
|
app.use('/images', express.static('./server/images'));
|
||||||
app.use('/fonts', express.static(path.join(__dirname, './server/fonts')));
|
app.use('/fonts', express.static('./server/fonts'));
|
||||||
app.use('/scripts', express.static(path.join(__dirname, './server/scripts')));
|
app.use('/scripts', express.static('./server/scripts'));
|
||||||
app.use('/', express.static(path.join(__dirname, './dist')));
|
app.use('/', express.static('./dist'));
|
||||||
} else {
|
} else {
|
||||||
// debugging
|
// debugging
|
||||||
app.get('/index.html', index);
|
app.get('/index.html', index);
|
||||||
app.get('/', index);
|
app.get('/', index);
|
||||||
app.get('*', express.static(path.join(__dirname, './server')));
|
app.get('*', express.static('./server'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
3671
package-lock.json
generated
3671
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.13.4",
|
"version": "5.15.1",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.js",
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"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",
|
||||||
"lint": "eslint ./server/scripts/**/*.mjs",
|
"lint": "eslint ./server/scripts/**/*.mjs",
|
||||||
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
|
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
|
||||||
},
|
},
|
||||||
@@ -20,32 +22,33 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/netbymatt/ws4kp#readme",
|
"homepage": "https://github.com/netbymatt/ws4kp#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"del": "^7.1.0",
|
|
||||||
"jquery": "^3.6.0",
|
|
||||||
"jquery-touchswipe": "^1.6.19",
|
|
||||||
"luxon": "^3.0.0",
|
|
||||||
"nosleep.js": "^0.12.0",
|
|
||||||
"suncalc": "^1.8.0",
|
|
||||||
"swiped-events": "^1.1.4",
|
|
||||||
"@aws-sdk/client-cloudfront": "^3.609.0",
|
"@aws-sdk/client-cloudfront": "^3.609.0",
|
||||||
"gulp-awspublish": "^8.0.0",
|
"del": "^8.0.0",
|
||||||
"gulp-s3-upload": "^1.7.3",
|
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.2.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",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
|
"gulp-awspublish": "^8.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-htmlmin": "^5.0.1",
|
"gulp-htmlmin": "^5.0.1",
|
||||||
"gulp-rename": "^2.0.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-sass": "^5.1.0",
|
"gulp-s3-upload": "^1.7.3",
|
||||||
|
"gulp-sass": "^6.0.0",
|
||||||
"gulp-terser": "^2.0.0",
|
"gulp-terser": "^2.0.0",
|
||||||
|
"jquery": "^3.6.0",
|
||||||
|
"jquery-touchswipe": "^1.6.19",
|
||||||
|
"luxon": "^3.0.0",
|
||||||
|
"nosleep.js": "^0.12.0",
|
||||||
|
"sass": "^1.54.0",
|
||||||
|
"suncalc": "^1.8.0",
|
||||||
|
"swiped-events": "^1.1.4",
|
||||||
"terser-webpack-plugin": "^5.3.6",
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"webpack-stream": "^7.0.0",
|
"webpack-stream": "^7.0.0"
|
||||||
"sass": "^1.54.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"ejs": "^3.1.5",
|
||||||
"ejs": "^3.1.5"
|
"express": "^4.17.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 455 B |
BIN
server/images/social/1200x600.png
Normal file
BIN
server/images/social/1200x600.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
BIN
server/music/default/Catch the Sun.mp3
Normal file
BIN
server/music/default/Catch the Sun.mp3
Normal file
Binary file not shown.
BIN
server/music/default/Crisp day.mp3
Normal file
BIN
server/music/default/Crisp day.mp3
Normal file
Binary file not shown.
BIN
server/music/default/Rolling Clouds.mp3
Normal file
BIN
server/music/default/Rolling Clouds.mp3
Normal file
Binary file not shown.
BIN
server/music/default/Strong Breeze.mp3
Normal file
BIN
server/music/default/Strong Breeze.mp3
Normal file
Binary file not shown.
3
server/music/readme.txt
Normal file
3
server/music/readme.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.mp3 files placed in this folder will be available via the un-mute button in the application.
|
||||||
|
No subdirectories will be scanned, and music will be played in a random order.
|
||||||
|
The default folder will be used only if no .mp3 files are found in this /server/music folder
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
// it is intended to allow for customizations that do not get published back to the git repo
|
// it is intended to allow for customizations that do not get published back to the git repo
|
||||||
// for example, changing the logo
|
// for example, changing the logo
|
||||||
|
|
||||||
// start running after all content is loaded
|
const customTask = () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// get all of the logo images
|
// get all of the logo images
|
||||||
const logos = document.querySelectorAll('.logo img');
|
const logos = document.querySelectorAll('.logo img');
|
||||||
// loop through each logo
|
// loop through each logo
|
||||||
@@ -11,4 +10,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// change the source
|
// change the source
|
||||||
elem.src = 'my-custom-logo.gif';
|
elem.src = 'my-custom-logo.gif';
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// start running after all content is loaded, or immediately if page content is already loaded
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
// Loading hasn't finished yet
|
||||||
|
document.addEventListener('DOMContentLoaded', customTask);
|
||||||
|
} else {
|
||||||
|
// `DOMContentLoaded` has already fired
|
||||||
|
customTask();
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import settings from './modules/settings.mjs';
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
init();
|
init();
|
||||||
|
getCustomCode();
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
@@ -413,3 +414,15 @@ const fullScreenResizeCheck = () => {
|
|||||||
// store state of fullscreen element for next change detection
|
// store state of fullscreen element for next change detection
|
||||||
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
|
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCustomCode = async () => {
|
||||||
|
// fetch the custom file and see if it returns a 200 status
|
||||||
|
const response = await fetch('scripts/custom.js', { method: 'HEAD' });
|
||||||
|
if (response.ok) {
|
||||||
|
// add the script element to the page
|
||||||
|
const customElem = document.createElement('script');
|
||||||
|
customElem.src = 'scripts/custom.js';
|
||||||
|
customElem.type = 'text/javascript';
|
||||||
|
document.body.append(customElem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { loadImg, preloadImg } from './utils/image.mjs';
|
|||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
|
|
||||||
class Almanac extends WeatherDisplay {
|
class Almanac extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -123,10 +123,10 @@ class Almanac extends WeatherDisplay {
|
|||||||
// sun and moon data
|
// sun and moon data
|
||||||
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
|
|
||||||
const days = info.moon.map((MoonPhase) => {
|
const days = info.moon.map((MoonPhase) => {
|
||||||
const fill = {};
|
const fill = {};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
|||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
import {
|
import {
|
||||||
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
|
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
|
||||||
} from './utils/units.mjs';
|
} from './utils/units.mjs';
|
||||||
|
|
||||||
// some stations prefixed do not provide all the necessary data
|
// some stations prefixed do not provide all the necessary data
|
||||||
@@ -159,23 +159,32 @@ const shortConditions = (_condition) => {
|
|||||||
|
|
||||||
// format the received data
|
// format the received data
|
||||||
const parseData = (data) => {
|
const parseData = (data) => {
|
||||||
|
// get the unit converter
|
||||||
|
const windConverter = windSpeed();
|
||||||
|
const temperatureConverter = temperature();
|
||||||
|
const metersConverter = distanceMeters();
|
||||||
|
const kilometersConverter = distanceKilometers();
|
||||||
|
const pressureConverter = pressure();
|
||||||
|
|
||||||
const observations = data.features[0].properties;
|
const observations = data.features[0].properties;
|
||||||
// values from api are provided in metric
|
// values from api are provided in metric
|
||||||
data.observations = observations;
|
data.observations = observations;
|
||||||
data.Temperature = Math.round(observations.temperature.value);
|
data.Temperature = temperatureConverter(observations.temperature.value);
|
||||||
data.TemperatureUnit = 'C';
|
data.TemperatureUnit = temperatureConverter.units;
|
||||||
data.DewPoint = Math.round(observations.dewpoint.value);
|
data.DewPoint = temperatureConverter(observations.dewpoint.value);
|
||||||
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
data.Ceiling = metersConverter(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||||
data.CeilingUnit = 'm.';
|
data.CeilingUnit = metersConverter.units;
|
||||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
data.Visibility = kilometersConverter(observations.visibility.value);
|
||||||
data.VisibilityUnit = ' km.';
|
data.VisibilityUnit = kilometersConverter.units;
|
||||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
data.Pressure = pressureConverter(observations.barometricPressure.value);
|
||||||
|
data.PressureUnit = pressureConverter.units;
|
||||||
|
data.HeatIndex = temperatureConverter(observations.heatIndex.value);
|
||||||
|
data.WindChill = temperatureConverter(observations.windChill.value);
|
||||||
|
data.WindSpeed = windConverter(observations.windSpeed.value);
|
||||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||||
data.Pressure = Math.round(observations.barometricPressure.value);
|
data.WindGust = windConverter(observations.windGust.value);
|
||||||
data.HeatIndex = Math.round(observations.heatIndex.value);
|
data.WindSpeed = windConverter(data.WindSpeed);
|
||||||
data.WindChill = Math.round(observations.windChill.value);
|
data.WindUnit = windConverter.units;
|
||||||
data.WindGust = Math.round(observations.windGust.value);
|
|
||||||
data.WindUnit = 'KPH';
|
|
||||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||||
data.PressureDirection = '';
|
data.PressureDirection = '';
|
||||||
@@ -186,20 +195,6 @@ const parseData = (data) => {
|
|||||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||||
|
|
||||||
// convert to us units
|
|
||||||
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
|
||||||
data.TemperatureUnit = 'F';
|
|
||||||
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
|
||||||
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
|
||||||
data.CeilingUnit = 'ft.';
|
|
||||||
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
|
||||||
data.VisibilityUnit = ' mi.';
|
|
||||||
data.WindSpeed = kphToMph(data.WindSpeed);
|
|
||||||
data.WindUnit = 'MPH';
|
|
||||||
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
|
||||||
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
|
||||||
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
|
||||||
data.WindGust = kphToMph(data.WindGust);
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const screens = [
|
|||||||
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
||||||
|
|
||||||
// barometric pressure
|
// barometric pressure
|
||||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
|
||||||
|
|
||||||
// wind
|
// wind
|
||||||
(data) => {
|
(data) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
|||||||
import { preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
class ExtendedForecast extends WeatherDisplay {
|
class ExtendedForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -26,7 +27,7 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
try {
|
try {
|
||||||
forecast = await json(weatherParameters.forecast, {
|
forecast = await json(weatherParameters.forecast, {
|
||||||
data: {
|
data: {
|
||||||
units: 'us',
|
units: settings.units.value,
|
||||||
},
|
},
|
||||||
retryCount: 3,
|
retryCount: 3,
|
||||||
stillWaiting: () => this.stillWaiting(),
|
stillWaiting: () => this.stillWaiting(),
|
||||||
@@ -131,7 +132,7 @@ const shortenExtendedForecastText = (long) => {
|
|||||||
[/dense /gi, ''],
|
[/dense /gi, ''],
|
||||||
[/Thunderstorm/g, 'T\'Storm'],
|
[/Thunderstorm/g, 'T\'Storm'],
|
||||||
];
|
];
|
||||||
// run all regexes
|
// run all regexes
|
||||||
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
||||||
|
|
||||||
let conditions = short.split(' ');
|
let conditions = short.split(' ');
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// base count change callback
|
// base count change callback
|
||||||
baseCountChange(count) {
|
baseCountChange(count) {
|
||||||
// calculate scroll offset and don't go past end
|
// calculate scroll offset and don't go past end
|
||||||
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').getBoundingClientRect().height - 390, (count - 150));
|
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').offsetHeight - 390, (count - 150));
|
||||||
|
|
||||||
// don't let offset go negative
|
// don't let offset go negative
|
||||||
if (offsetY < 0) offsetY = 0;
|
if (offsetY < 0) offsetY = 0;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import getHourlyData from './hourly.mjs';
|
import getHourlyData from './hourly.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
|
||||||
class HourlyGraph extends WeatherDisplay {
|
class HourlyGraph extends WeatherDisplay {
|
||||||
@@ -38,7 +38,7 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
const skyCover = data.map((d) => d.skyCover);
|
const skyCover = data.map((d) => d.skyCover);
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
skyCover, temperature, probabilityOfPrecipitation,
|
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
@@ -107,6 +107,9 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
// set the image source
|
// set the image source
|
||||||
this.image.src = canvas.toDataURL();
|
this.image.src = canvas.toDataURL();
|
||||||
|
|
||||||
|
// change the units in the header
|
||||||
|
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||||
|
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,7 @@ const drawPath = (path, ctx, options) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// format as 1p, 12a, etc.
|
// format as 1p, 12a, etc.
|
||||||
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
|
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
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 { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
|
import { temperature as temperatureUnit, distanceKilometers } 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';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import getSun from './almanac.mjs';
|
import getSun from './almanac.mjs';
|
||||||
|
|
||||||
class Hourly extends WeatherDisplay {
|
class Hourly extends WeatherDisplay {
|
||||||
@@ -56,7 +56,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const list = this.elem.querySelector('.hourly-lines');
|
const list = this.elem.querySelector('.hourly-lines');
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
const startingHour = DateTime.local();
|
const startingHour = DateTime.local().setZone(timeZone());
|
||||||
|
|
||||||
const lines = this.data.map((data, index) => {
|
const lines = this.data.map((data, index) => {
|
||||||
const fillValues = {};
|
const fillValues = {};
|
||||||
@@ -66,8 +66,8 @@ class Hourly extends WeatherDisplay {
|
|||||||
fillValues.hour = formattedHour;
|
fillValues.hour = formattedHour;
|
||||||
|
|
||||||
// temperatures, convert to strings with no decimal
|
// temperatures, convert to strings with no decimal
|
||||||
const temperature = Math.round(data.temperature).toString().padStart(3);
|
const temperature = data.temperature.toString().padStart(3);
|
||||||
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
|
const feelsLike = data.apparentTemperature.toString().padStart(3);
|
||||||
fillValues.temp = temperature;
|
fillValues.temp = temperature;
|
||||||
// only plot apparent temperature if there is a difference
|
// only plot apparent temperature if there is a difference
|
||||||
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
|
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
|
||||||
@@ -132,6 +132,11 @@ class Hourly extends WeatherDisplay {
|
|||||||
|
|
||||||
// extract specific values from forecast and format as an array
|
// extract specific values from forecast and format as an array
|
||||||
const parseForecast = async (data) => {
|
const parseForecast = async (data) => {
|
||||||
|
// get unit converters
|
||||||
|
const temperatureConverter = temperatureUnit();
|
||||||
|
const distanceConverter = distanceKilometers();
|
||||||
|
|
||||||
|
// parse data
|
||||||
const temperature = expand(data.temperature.values);
|
const temperature = expand(data.temperature.values);
|
||||||
const apparentTemperature = expand(data.apparentTemperature.values);
|
const apparentTemperature = expand(data.apparentTemperature.values);
|
||||||
const windSpeed = expand(data.windSpeed.values);
|
const windSpeed = expand(data.windSpeed.values);
|
||||||
@@ -145,9 +150,11 @@ const parseForecast = async (data) => {
|
|||||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||||
|
|
||||||
return temperature.map((val, idx) => ({
|
return temperature.map((val, idx) => ({
|
||||||
temperature: celsiusToFahrenheit(temperature[idx]),
|
temperature: temperatureConverter(temperature[idx]),
|
||||||
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
|
temperatureUnit: temperatureConverter.units,
|
||||||
windSpeed: kilometersToMiles(windSpeed[idx]),
|
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
||||||
|
windSpeed: distanceConverter(windSpeed[idx]),
|
||||||
|
windUnit: distanceConverter.units,
|
||||||
windDirection: directionToNSEW(windDirection[idx]),
|
windDirection: directionToNSEW(windDirection[idx]),
|
||||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||||
skyCover: skyCover[idx],
|
skyCover: skyCover[idx],
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { distance as calcDistance, directionToNSEW } from './utils/calc.mjs';
|
|||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { locationCleanup } from './utils/string.mjs';
|
import { locationCleanup } from './utils/string.mjs';
|
||||||
import { celsiusToFahrenheit, kphToMph } from './utils/units.mjs';
|
import { temperature, windSpeed } from './utils/units.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
class LatestObservations extends WeatherDisplay {
|
class LatestObservations extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -64,14 +65,22 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
// sort array by station name
|
// sort array by station name
|
||||||
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
|
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
|
||||||
|
|
||||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
if (settings.units.value === 'us') {
|
||||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||||
|
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||||
|
} else {
|
||||||
|
this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
|
||||||
|
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
|
||||||
|
}
|
||||||
|
// get unit converters
|
||||||
|
const windConverter = windSpeed();
|
||||||
|
const temperatureConverter = temperature();
|
||||||
|
|
||||||
const lines = sortedConditions.map((condition) => {
|
const lines = sortedConditions.map((condition) => {
|
||||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||||
|
|
||||||
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
|
const Temperature = temperatureConverter(condition.temperature.value);
|
||||||
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
|
const WindSpeed = windConverter(condition.windSpeed.value);
|
||||||
|
|
||||||
const fill = {
|
const fill = {
|
||||||
location: locationCleanup(condition.city).substr(0, 14),
|
location: locationCleanup(condition.city).substr(0, 14),
|
||||||
@@ -94,6 +103,8 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
linesContainer.innerHTML = '';
|
linesContainer.innerHTML = '';
|
||||||
linesContainer.append(...lines);
|
linesContainer.append(...lines);
|
||||||
|
|
||||||
|
// update temperature unit header
|
||||||
|
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,8 +133,8 @@ const getStations = async (stations) => {
|
|||||||
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
|
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
|
||||||
// test for temperature, weather and wind values present
|
// test for temperature, weather and wind values present
|
||||||
if (data.properties.temperature.value === null
|
if (data.properties.temperature.value === null
|
||||||
|| data.properties.textDescription === ''
|
|| data.properties.textDescription === ''
|
||||||
|| data.properties.windSpeed.value === null) return false;
|
|| data.properties.windSpeed.value === null) return false;
|
||||||
// format the return values
|
// format the return values
|
||||||
return {
|
return {
|
||||||
...data.properties,
|
...data.properties,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import STATUS from './status.mjs';
|
|||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
class LocalForecast extends WeatherDisplay {
|
class LocalForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -61,7 +62,7 @@ class LocalForecast extends WeatherDisplay {
|
|||||||
try {
|
try {
|
||||||
return await json(weatherParameters.forecast, {
|
return await json(weatherParameters.forecast, {
|
||||||
data: {
|
data: {
|
||||||
units: 'us',
|
units: settings.units.value,
|
||||||
},
|
},
|
||||||
retryCount: 3,
|
retryCount: 3,
|
||||||
stillWaiting: () => this.stillWaiting(),
|
stillWaiting: () => this.stillWaiting(),
|
||||||
|
|||||||
163
server/scripts/modules/media.mjs
Normal file
163
server/scripts/modules/media.mjs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { json } from './utils/fetch.mjs';
|
||||||
|
import Setting from './utils/setting.mjs';
|
||||||
|
|
||||||
|
let playlist;
|
||||||
|
let currentTrack = 0;
|
||||||
|
let player;
|
||||||
|
|
||||||
|
const mediaPlaying = new Setting('mediaPlaying', 'Media Playing', 'boolean', false, null, true);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// add the event handler to the page
|
||||||
|
document.getElementById('ToggleMedia').addEventListener('click', toggleMedia);
|
||||||
|
// get the playlist
|
||||||
|
getMedia();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMedia = async () => {
|
||||||
|
try {
|
||||||
|
// fetch the playlist
|
||||||
|
const rawPlaylist = await json('playlist.json');
|
||||||
|
// store the playlist
|
||||||
|
playlist = rawPlaylist;
|
||||||
|
// enable the media player
|
||||||
|
enableMediaPlayer();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Couldn't get playlist");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableMediaPlayer = () => {
|
||||||
|
// see if files are available
|
||||||
|
if (playlist?.availableFiles?.length > 0) {
|
||||||
|
// randomize the list
|
||||||
|
randomizePlaylist();
|
||||||
|
// enable the icon
|
||||||
|
const icon = document.getElementById('ToggleMedia');
|
||||||
|
icon.classList.add('available');
|
||||||
|
// set the button type
|
||||||
|
setIcon();
|
||||||
|
// if we're already playing (sticky option) then try to start playing
|
||||||
|
if (mediaPlaying.value === true) {
|
||||||
|
startMedia();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setIcon = () => {
|
||||||
|
// get the icon
|
||||||
|
const icon = document.getElementById('ToggleMedia');
|
||||||
|
if (mediaPlaying.value === true) {
|
||||||
|
icon.classList.add('playing');
|
||||||
|
} else {
|
||||||
|
icon.classList.remove('playing');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMedia = (forcedState) => {
|
||||||
|
// handle forcing
|
||||||
|
if (typeof forcedState === 'boolean') {
|
||||||
|
mediaPlaying.value = forcedState;
|
||||||
|
} else {
|
||||||
|
// toggle the state
|
||||||
|
mediaPlaying.value = !mediaPlaying.value;
|
||||||
|
}
|
||||||
|
// handle the state change
|
||||||
|
stateChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMedia = async () => {
|
||||||
|
// if there's not media player yet, enable it
|
||||||
|
if (!player) {
|
||||||
|
initializePlayer();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await player.play();
|
||||||
|
} catch (e) {
|
||||||
|
// report the error
|
||||||
|
console.error('Couldn\'t play music');
|
||||||
|
console.error(e);
|
||||||
|
// set state back to not playing for good UI experience
|
||||||
|
mediaPlaying.value = false;
|
||||||
|
stateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopMedia = () => {
|
||||||
|
if (!player) return;
|
||||||
|
player.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateChanged = () => {
|
||||||
|
// update the icon
|
||||||
|
setIcon();
|
||||||
|
// react to the new state
|
||||||
|
if (mediaPlaying.value) {
|
||||||
|
startMedia();
|
||||||
|
} else {
|
||||||
|
stopMedia();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomizePlaylist = () => {
|
||||||
|
let availableFiles = [...playlist.availableFiles];
|
||||||
|
const randomPlaylist = [];
|
||||||
|
while (availableFiles.length > 0) {
|
||||||
|
// get a randon item from the available files
|
||||||
|
const i = Math.floor(Math.random() * availableFiles.length);
|
||||||
|
// add it to the final list
|
||||||
|
randomPlaylist.push(availableFiles[i]);
|
||||||
|
// remove the file from the available files
|
||||||
|
availableFiles = availableFiles.filter((file, index) => index !== i);
|
||||||
|
}
|
||||||
|
playlist.availableFiles = randomPlaylist;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializePlayer = () => {
|
||||||
|
// basic sanity checks
|
||||||
|
if (!playlist.availableFiles || playlist?.availableFiles.length === 0) {
|
||||||
|
throw new Error('No playlist available');
|
||||||
|
}
|
||||||
|
if (player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create the player
|
||||||
|
player = new Audio();
|
||||||
|
|
||||||
|
// reset the playlist index
|
||||||
|
currentTrack = 0;
|
||||||
|
|
||||||
|
// add event handlers
|
||||||
|
player.addEventListener('canplay', playerCanPlay);
|
||||||
|
player.addEventListener('ended', playerEnded);
|
||||||
|
|
||||||
|
// get the first file
|
||||||
|
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||||
|
player.type = 'audio/mpeg';
|
||||||
|
};
|
||||||
|
|
||||||
|
const playerCanPlay = async () => {
|
||||||
|
// check to make sure they user still wants music (protect against slow loading music)
|
||||||
|
if (!mediaPlaying.value) return;
|
||||||
|
// start playing
|
||||||
|
startMedia();
|
||||||
|
};
|
||||||
|
|
||||||
|
const playerEnded = () => {
|
||||||
|
// next track
|
||||||
|
currentTrack += 1;
|
||||||
|
// roll over and re-randomize the tracks
|
||||||
|
if (currentTrack >= playlist.availableFiles) {
|
||||||
|
randomizePlaylist();
|
||||||
|
currentTrack = 0;
|
||||||
|
}
|
||||||
|
// update the player source
|
||||||
|
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
toggleMedia,
|
||||||
|
};
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||||
import { preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
|
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||||
|
|
||||||
const buildForecast = (forecast, city, cityXY) => ({
|
const buildForecast = (forecast, city, cityXY) => {
|
||||||
daytime: forecast.isDaytime,
|
// get a unit converter
|
||||||
temperature: forecast.temperature || 0,
|
const temperatureConverter = temperatureUnit('us');
|
||||||
name: formatCity(city.city),
|
return {
|
||||||
icon: forecast.icon,
|
daytime: forecast.isDaytime,
|
||||||
x: cityXY.x,
|
temperature: temperatureConverter(forecast.temperature || 0),
|
||||||
y: cityXY.y,
|
name: formatCity(city.city),
|
||||||
time: forecast.startTime,
|
icon: forecast.icon,
|
||||||
});
|
x: cityXY.x,
|
||||||
|
y: cityXY.y,
|
||||||
|
time: forecast.startTime,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getRegionalObservation = async (point, city) => {
|
const getRegionalObservation = async (point, city) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { distance as calcDistance } from './utils/calc.mjs';
|
import { distance as calcDistance } from './utils/calc.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import { celsiusToFahrenheit } from './utils/units.mjs';
|
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||||
import { preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
@@ -59,7 +59,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
const regionalCities = [];
|
const regionalCities = [];
|
||||||
combinedCities.forEach((city) => {
|
combinedCities.forEach((city) => {
|
||||||
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
|
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
|
||||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||||
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
|
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
|
||||||
const targetDist = city.targetDistance || 1;
|
const targetDist = city.targetDistance || 1;
|
||||||
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
||||||
@@ -71,6 +71,9 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// get a unit converter
|
||||||
|
const temperatureConverter = temperatureUnit();
|
||||||
|
|
||||||
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
||||||
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
||||||
try {
|
try {
|
||||||
@@ -93,7 +96,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
// format the observation the same as the forecast
|
// format the observation the same as the forecast
|
||||||
const regionalObservation = {
|
const regionalObservation = {
|
||||||
daytime: !!/\/day\//.test(observation.icon),
|
daytime: !!/\/day\//.test(observation.icon),
|
||||||
temperature: celsiusToFahrenheit(observation.temperature.value),
|
temperature: temperatureConverter(observation.temperature.value),
|
||||||
name: utils.formatCity(city.city),
|
name: utils.formatCity(city.city),
|
||||||
icon: observation.icon,
|
icon: observation.icon,
|
||||||
x: cityXY.x,
|
x: cityXY.x,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const settings = { speed: { value: 1.0 } };
|
|||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
// create settings
|
// create settings
|
||||||
settings.wide = new Setting('wide', 'Widescreen', 'boolean', false, wideScreenChange, true);
|
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
|
||||||
settings.kiosk = new Setting('kiosk', 'Kiosk', 'boolean', false, kioskChange, false);
|
settings.kiosk = new Setting('kiosk', 'Kiosk', 'checkbox', false, kioskChange, false);
|
||||||
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
|
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
|
||||||
[0.5, 'Very Fast'],
|
[0.5, 'Very Fast'],
|
||||||
[0.75, 'Fast'],
|
[0.75, 'Fast'],
|
||||||
@@ -18,6 +18,10 @@ const init = () => {
|
|||||||
[1.25, 'Slow'],
|
[1.25, 'Slow'],
|
||||||
[1.5, 'Very Slow'],
|
[1.5, 'Very Slow'],
|
||||||
]);
|
]);
|
||||||
|
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
|
||||||
|
['us', 'US'],
|
||||||
|
['si', 'Metric'],
|
||||||
|
]);
|
||||||
|
|
||||||
// generate html objects
|
// generate html objects
|
||||||
const settingHtml = Object.values(settings).map((d) => d.generate());
|
const settingHtml = Object.values(settings).map((d) => d.generate());
|
||||||
@@ -47,4 +51,13 @@ const kioskChange = (value) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unitChange = () => {
|
||||||
|
// reload the data at the top level to refresh units
|
||||||
|
// after the initial load
|
||||||
|
if (unitChange.firstRunDone) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
unitChange.firstRunDone = true;
|
||||||
|
};
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
|||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
class TravelForecast extends WeatherDisplay {
|
class TravelForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId, defaultActive) {
|
constructor(navId, elemId, defaultActive) {
|
||||||
@@ -34,7 +35,11 @@ class TravelForecast extends WeatherDisplay {
|
|||||||
try {
|
try {
|
||||||
// get point then forecast
|
// get point then forecast
|
||||||
if (!city.point) throw new Error('No pre-loaded point');
|
if (!city.point) throw new Error('No pre-loaded point');
|
||||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`, {
|
||||||
|
data: {
|
||||||
|
units: settings.units.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
// determine today or tomorrow (shift periods by 1 if tomorrow)
|
// determine today or tomorrow (shift periods by 1 if tomorrow)
|
||||||
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
|
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
|
||||||
// return a pared-down forecast
|
// return a pared-down forecast
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ class Setting {
|
|||||||
if (type === 'select' && urlValue !== undefined) {
|
if (type === 'select' && urlValue !== undefined) {
|
||||||
urlState = parseFloat(urlValue);
|
urlState = parseFloat(urlValue);
|
||||||
}
|
}
|
||||||
|
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
||||||
|
// couldn't parse as a float, store as a string
|
||||||
|
urlState = urlValue;
|
||||||
|
}
|
||||||
|
|
||||||
// get existing value if present
|
// get existing value if present
|
||||||
const storedValue = urlState ?? this.getFromLocalStorage();
|
const storedValue = urlState ?? this.getFromLocalStorage();
|
||||||
@@ -59,7 +63,11 @@ class Setting {
|
|||||||
|
|
||||||
this.values.forEach(([value, text]) => {
|
this.values.forEach(([value, text]) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = value.toFixed(2);
|
if (typeof value === 'number') {
|
||||||
|
option.value = value.toFixed(2);
|
||||||
|
} else {
|
||||||
|
option.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
option.innerHTML = text;
|
option.innerHTML = text;
|
||||||
select.append(option);
|
select.append(option);
|
||||||
@@ -108,6 +116,10 @@ class Setting {
|
|||||||
selectChange(e) {
|
selectChange(e) {
|
||||||
// update the value
|
// update the value
|
||||||
this.myValue = parseFloat(e.target.value);
|
this.myValue = parseFloat(e.target.value);
|
||||||
|
if (Number.isNaN(this.myValue)) {
|
||||||
|
// was a string, store as such
|
||||||
|
this.myValue = e.target.value;
|
||||||
|
}
|
||||||
this.storeToLocalStorage(this.myValue);
|
this.storeToLocalStorage(this.myValue);
|
||||||
|
|
||||||
// call the change action
|
// call the change action
|
||||||
@@ -155,6 +167,8 @@ class Setting {
|
|||||||
case 'select':
|
case 'select':
|
||||||
this.selectHighlight(newValue);
|
this.selectHighlight(newValue);
|
||||||
break;
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
break;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
default:
|
default:
|
||||||
this.element.checked = newValue;
|
this.element.checked = newValue;
|
||||||
@@ -168,7 +182,7 @@ class Setting {
|
|||||||
selectHighlight(newValue) {
|
selectHighlight(newValue) {
|
||||||
// set the dropdown to the provided value
|
// set the dropdown to the provided value
|
||||||
this.element.querySelectorAll('option').forEach((elem) => {
|
this.element.querySelectorAll('option').forEach((elem) => {
|
||||||
elem.selected = newValue.toFixed(2) === elem.value;
|
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,113 @@
|
|||||||
|
// get the settings for units
|
||||||
|
import settings from '../settings.mjs';
|
||||||
// *********************************** unit conversions ***********************
|
// *********************************** unit conversions ***********************
|
||||||
|
|
||||||
|
// round 2 provided for lat/lon formatting
|
||||||
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
|
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
|
||||||
|
|
||||||
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
|
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
|
||||||
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
||||||
|
const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9);
|
||||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
||||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
||||||
|
|
||||||
|
// each module/page/slide creates it's own unit converter as needed by providing the base units available
|
||||||
|
// the factory function then returns an appropriate converter or pass-thru function for use on the page
|
||||||
|
|
||||||
|
const windSpeed = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru
|
||||||
|
let converter = (passthru) => Math.round(passthru);
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
converter = kphToMph;
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = 'kph';
|
||||||
|
} else {
|
||||||
|
converter.units = 'MPH';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const temperature = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru
|
||||||
|
let converter = (passthru) => Math.round(passthru);
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
if (defaultUnit === 'us') {
|
||||||
|
converter = fahrenheitToCelsius;
|
||||||
|
} else {
|
||||||
|
converter = celsiusToFahrenheit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = 'C';
|
||||||
|
} else {
|
||||||
|
converter.units = 'F';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceMeters = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru
|
||||||
|
let converter = (passthru) => Math.round(passthru);
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
// rounded to the nearest 100 (ceiling)
|
||||||
|
converter = (value) => Math.round(metersToFeet(value) / 100) * 100;
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = 'm.';
|
||||||
|
} else {
|
||||||
|
converter.units = 'ft.';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceKilometers = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru
|
||||||
|
let converter = (passthru) => Math.round(passthru / 1000);
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
converter = (value) => Math.round(kilometersToMiles(value) / 1000);
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = ' km.';
|
||||||
|
} else {
|
||||||
|
converter.units = ' mi.';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pressure = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru (millibar)
|
||||||
|
let converter = (passthru) => Math.round(passthru / 100);
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
converter = (value) => pascalToInHg(value).toFixed(2);
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = ' mbar';
|
||||||
|
} else {
|
||||||
|
converter.units = ' in.hg';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
kphToMph,
|
// unit conversions
|
||||||
celsiusToFahrenheit,
|
windSpeed,
|
||||||
kilometersToMiles,
|
temperature,
|
||||||
metersToFeet,
|
distanceMeters,
|
||||||
pascalToInHg,
|
distanceKilometers,
|
||||||
|
pressure,
|
||||||
|
|
||||||
|
// formatter
|
||||||
round2,
|
round2,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
34
server/styles/scss/_media.scss
Normal file
34
server/styles/scss/_media.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.media {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ToggleMedia {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.available {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
img.on {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.off {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon switch is handled by adding/removing the .playing class
|
||||||
|
&.playing {
|
||||||
|
img.on {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.off {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@
|
|||||||
@import 'radar';
|
@import 'radar';
|
||||||
@import 'regional-forecast';
|
@import 'regional-forecast';
|
||||||
@import 'almanac';
|
@import 'almanac';
|
||||||
@import 'hazards';
|
@import 'hazards';
|
||||||
|
@import 'media';
|
||||||
20
src/playlist-reader.mjs
Normal file
20
src/playlist-reader.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
const mp3Filter = (file) => file.match(/\.mp3$/);
|
||||||
|
|
||||||
|
const reader = async () => {
|
||||||
|
// get the listing of files in the folder
|
||||||
|
const rawFiles = await fs.readdir('./server/music');
|
||||||
|
// filter for mp3 files
|
||||||
|
const files = rawFiles.filter(mp3Filter);
|
||||||
|
// if files were found return them
|
||||||
|
if (files.length > 0) {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to the default folder
|
||||||
|
const defaultFiles = await fs.readdir('./server/music/default');
|
||||||
|
return defaultFiles.map((file) => `default/${file}`).filter(mp3Filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reader;
|
||||||
17
src/playlist.mjs
Normal file
17
src/playlist.mjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import reader from './playlist-reader.mjs';
|
||||||
|
|
||||||
|
const playlistGenerator = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const availableFiles = await reader();
|
||||||
|
res.json({
|
||||||
|
availableFiles,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.json({
|
||||||
|
availableFiles: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default playlistGenerator;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
@@ -10,17 +10,20 @@
|
|||||||
<meta name="author" content="Matt Walsh" />
|
<meta name="author" content="Matt Walsh" />
|
||||||
<meta name="application-name" content="WeatherStar 4000+" />
|
<meta name="application-name" content="WeatherStar 4000+" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<link rel="icon" href="images/Logo192.png" />
|
<link rel="icon" href="images/Logo192.png" />
|
||||||
|
<link rel="preload" href="playlist.json" as="fetch"/>
|
||||||
|
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="627">
|
||||||
|
|
||||||
<% if (production) { %>
|
<% if (production) { %>
|
||||||
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
||||||
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
|
||||||
<script type="text/javascript" src="resources/vendor.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/vendor.min.js?_=<%=production%>"></script>
|
||||||
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
|
||||||
<script type="text/javascript" src="scripts/custom.js?_=<%=production%>"></script>
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
||||||
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script>
|
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script>
|
||||||
@@ -44,15 +47,12 @@
|
|||||||
<script type="module" src="scripts/modules/progress.mjs"></script>
|
<script type="module" src="scripts/modules/progress.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||||
|
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||||
<script type="module" src="scripts/index.mjs"></script>
|
<script type="module" src="scripts/index.mjs"></script>
|
||||||
<script type="text/javascript" src="scripts/custom.js"></script>
|
|
||||||
<!-- data -->
|
<!-- data -->
|
||||||
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
||||||
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
|
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
|
||||||
<script type="text/javascript" src="scripts/data/stations.js"></script>
|
<script type="text/javascript" src="scripts/data/stations.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="scripts/custom.js"></script>
|
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@@ -131,6 +131,10 @@
|
|||||||
<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">
|
||||||
|
<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="Unmute" />
|
||||||
|
</div>
|
||||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,6 +145,7 @@
|
|||||||
<div class="info">
|
<div class="info">
|
||||||
<a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a>
|
<a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="media"></div>
|
||||||
|
|
||||||
<div class='heading'>Selected displays</div>
|
<div class='heading'>Selected displays</div>
|
||||||
<div id='enabledDisplays'>
|
<div id='enabledDisplays'>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"devbridge",
|
"devbridge",
|
||||||
"gifs",
|
"gifs",
|
||||||
"ltrim",
|
"ltrim",
|
||||||
|
"mbar",
|
||||||
"Noaa",
|
"Noaa",
|
||||||
"nosleep",
|
"nosleep",
|
||||||
"Pngs",
|
"Pngs",
|
||||||
@@ -51,12 +52,15 @@
|
|||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.defaultFormatter": "j69.ejs-beautify"
|
"editor.defaultFormatter": "j69.ejs-beautify"
|
||||||
},
|
},
|
||||||
"files.exclude": {},
|
"files.exclude": {
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/debug.log": true,
|
||||||
|
"server/scripts/custom.js": true
|
||||||
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user