mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
cc05aafb95 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,4 +11,4 @@ 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
|
||||||
|
|||||||
@@ -179,7 +179,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.
|
||||||
@@ -321,6 +321,7 @@ Thanks to the WeatherStar+ community for providing these discussions to further
|
|||||||
* [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).
|
* [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.
|
* [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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4150
package-lock.json
generated
4150
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.1.9",
|
"version": "6.2.4",
|
||||||
"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",
|
||||||
@@ -50,13 +51,12 @@
|
|||||||
"swiped-events": "^1.1.4",
|
"swiped-events": "^1.1.4",
|
||||||
"terser-webpack-plugin": "^5.3.6",
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"webpack": "^5.99.9",
|
"webpack": "^5.99.9",
|
||||||
"webpack-stream": "^7.0.0"
|
"webpack-stream": "^7.0.0",
|
||||||
|
"metar-taf-parser": "^9.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0"
|
||||||
"metar-taf-parser": "^9.0.0",
|
|
||||||
"npm": "^11.6.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -91,18 +103,8 @@ const drawScreen = async () => {
|
|||||||
const thisScreen = workingScreens[screenIndex](scrollData);
|
const thisScreen = workingScreens[screenIndex](scrollData);
|
||||||
|
|
||||||
// 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));
|
|
||||||
});
|
|
||||||
// special case for red background on hazard scroll
|
|
||||||
const mainScrollBg = document.getElementById('scroll-bg');
|
|
||||||
mainScrollBg.className = '';
|
|
||||||
if (thisScreen?.classes?.includes('hazard')) {
|
|
||||||
mainScrollBg.classList.add('hazard');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof thisScreen === 'string') {
|
if (typeof thisScreen === 'string') {
|
||||||
// only a string
|
// only a string
|
||||||
@@ -131,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,
|
||||||
@@ -196,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
|
||||||
@@ -229,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;
|
||||||
@@ -252,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,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;
|
||||||
|
|
||||||
@@ -283,6 +284,8 @@ window.CurrentWeatherScroll = {
|
|||||||
addScreen,
|
addScreen,
|
||||||
reset,
|
reset,
|
||||||
start,
|
start,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
screenCount,
|
screenCount,
|
||||||
atDefault,
|
atDefault,
|
||||||
};
|
};
|
||||||
@@ -291,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(
|
||||||
|
|||||||
@@ -330,6 +330,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 +358,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 +388,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 +423,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 +464,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 +474,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 +532,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 +543,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)
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -5,21 +5,22 @@ import en from '../../vendor/auto/locale/en.js';
|
|||||||
|
|
||||||
// metar-taf-parser requires regex lookbehind
|
// metar-taf-parser requires regex lookbehind
|
||||||
// this does not work in iOS < 16.4
|
// this does not work in iOS < 16.4
|
||||||
// this is a detection algorithm for iOS versions
|
// this is a detection algorithm for missing lookbehind support
|
||||||
const isIos = /iP(ad|od|hone)/i.test(window.navigator.userAgent);
|
const supportsRegexLookAheadLookBehindCheck = () => {
|
||||||
let iosVersionOk = false;
|
try {
|
||||||
if (isIos) {
|
return (
|
||||||
// regex match the version string
|
// deliberately using RegExp for broader browser support during check
|
||||||
const iosVersionRaw = /OS (\d+)_(\d+)/.exec(window.navigator.userAgent);
|
/* eslint-disable prefer-regex-literals */
|
||||||
// check for match
|
'hibyehihi'
|
||||||
if (iosVersionRaw) {
|
.replace(new RegExp('(?<=hi)hi', 'g'), 'hello')
|
||||||
// break into parts
|
.replace(new RegExp('hi(?!bye)', 'g'), 'hey') === 'hibyeheyhello'
|
||||||
const iosVersionMajor = parseInt(iosVersionRaw[1], 10);
|
/* eslint-enable prefer-regex-literals */
|
||||||
const iosVersionMinor = parseInt(iosVersionRaw[2], 10);
|
);
|
||||||
if (iosVersionMajor > 16) iosVersionOk = true;
|
} catch {
|
||||||
if (iosVersionMajor === 16 && iosVersionMinor >= 4) iosVersionOk = true;
|
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
|
||||||
@@ -27,8 +28,8 @@ if (isIos) {
|
|||||||
* @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) => {
|
||||||
// check for a metar message and for unusable ios versions
|
// check for a metar message and for regex lookbehind support
|
||||||
if (!observation?.rawMessage || (isIos && !iosVersionOk)) {
|
if (!observation?.rawMessage || (!supportsRegexLookAheadLookBehind)) {
|
||||||
return observation;
|
return observation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -341,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,
|
||||||
|
|||||||
@@ -112,31 +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: 77px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 3px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&.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 {
|
||||||
@@ -158,21 +159,17 @@
|
|||||||
// left: calc((elem width) - 640px);
|
// left: calc((elem width) - 640px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#scroll-bg {
|
.wide #container>.scroll {
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
height: 77px;
|
|
||||||
width: 640px;
|
|
||||||
|
|
||||||
&.hazard {
|
|
||||||
background-color: rgb(112, 35, 35);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide #scroll-bg {
|
|
||||||
width: 854px;
|
width: 854px;
|
||||||
|
margin-left: -107px;
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
margin-left: 107px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -133,8 +133,8 @@
|
|||||||
<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 id="scroll-bg"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="divTwcBottom">
|
<div id="divTwcBottom">
|
||||||
<div id="divTwcBottomLeft">
|
<div id="divTwcBottomLeft">
|
||||||
@@ -186,16 +186,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,23 @@
|
|||||||
<%- 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="cloud">Cloud %</div>
|
||||||
<div class="rain">Precip %</div>
|
<div class="rain">Precip %</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="y-axis">
|
<div class="y-axis">
|
||||||
<div class="label l-1">75</div>
|
<div class="label l-1">75</div>
|
||||||
<div class="label l-2">65</div>
|
<div class="label l-2">65</div>
|
||||||
<div class="label l-3">55</div>
|
<div class="label l-3">55</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<img id="chart-area"></img>
|
<img id="chart-area"></img>
|
||||||
</div>
|
</div>
|
||||||
<div class="x-axis">
|
<div class="x-axis">
|
||||||
<div class="label l-1">12a</div>
|
<div class="label l-1">12a</div>
|
||||||
<div class="label l-2">6a</div>
|
<div class="label l-2">6a</div>
|
||||||
<div class="label l-3">12p</div>
|
<div class="label l-3">12p</div>
|
||||||
<div class="label l-4">6p</div>
|
<div class="label l-4">6p</div>
|
||||||
<div class="label l-5">12a</div>
|
<div class="label l-5">12a</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
|
||||||
@@ -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') %>
|
|
||||||
@@ -73,7 +73,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