Merge remote-tracking branch 'upstream/main' into modernization-and-refactor

This commit is contained in:
Eddy G
2025-07-02 09:08:07 -04:00
8 changed files with 211 additions and 17 deletions

View File

@@ -311,6 +311,9 @@ When using Docker:
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
### RSS feeds and custom scroll
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set.
## Issue reporting and feature requests
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. I've also observed that the API can go down on a regional basis (based on NWS office locations). This means that you may have problems getting data for, say, Chicago right now, but Dallas and others are working just fine.

View File

@@ -89,6 +89,7 @@ const mjsSources = [
'server/scripts/modules/travelforecast.mjs',
'server/scripts/modules/progress.mjs',
'server/scripts/modules/media.mjs',
'server/scripts/modules/custom-rss-feed.mjs',
'server/scripts/index.mjs',
];

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ws4kp",
"version": "5.26.2",
"version": "5.27.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ws4kp",
"version": "5.26.2",
"version": "5.27.0",
"license": "MIT",
"dependencies": {
"dotenv": "^16.5.0",

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.26.2",
"version": "5.27.0",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",

View File

@@ -18,6 +18,7 @@ let screenIndex = 0;
let sinceLastUpdate = 0;
let nextUpdate = DEFAULT_UPDATE;
let resetFlag;
let defaultScreensLoaded = true;
// start drawing conditions
// reset starts from the first item in the text scroll list
@@ -63,7 +64,7 @@ const incrementInterval = (force) => {
stop(display?.elemId === 'progress');
return;
}
screenIndex = (screenIndex + 1) % (lastScreen);
screenIndex = (screenIndex + 1) % (workingScreens.length);
// draw new text
drawScreen();
@@ -87,7 +88,7 @@ const drawScreen = async () => {
// if we have no current weather and no hazards, there's nothing to display
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
const thisScreen = screens[screenIndex](scrollData);
const thisScreen = workingScreens[screenIndex](scrollData);
// update classes on the scroll area
elemForEach('.weather-display .scroll', (elem) => {
@@ -136,8 +137,10 @@ const hazards = (data) => {
};
};
// additional screens are stored in a separate for simple clearing/resettings
let additionalScreens = [];
// the "screens" are stored in an array for easy addition and removal
const screens = [
const baseScreens = [
// hazards
hazards,
// station name
@@ -179,6 +182,9 @@ const screens = [
},
];
// working screens are the combination of base screens (when active) and additional screens
let workingScreens = [...baseScreens, ...additionalScreens];
// internal draw function with preset parameters
const drawCondition = (text) => {
// update all html scroll elements
@@ -194,19 +200,18 @@ const setHeader = (text) => {
});
};
// store the original number of screens
const originalScreens = screens.length;
let lastScreen = originalScreens;
// reset the number of screens
// reset the screens back to the original set
const reset = () => {
lastScreen = originalScreens;
workingScreens = [...baseScreens];
additionalScreens = [];
defaultScreensLoaded = true;
};
// add screen
const addScreen = (screen) => {
screens.push(screen);
lastScreen += 1;
// add screen, keepBase keeps the regular weather crawl
const addScreen = (screen, keepBase = true) => {
defaultScreensLoaded = false;
additionalScreens.push(screen);
workingScreens = [...(keepBase ? baseScreens : []), ...additionalScreens];
};
const drawScrollCondition = (screen) => {
@@ -259,6 +264,9 @@ const parseMessage = (event) => {
}
};
const screenCount = () => workingScreens.length;
const atDefault = () => defaultScreensLoaded;
// add event listener for start message
window.addEventListener('message', parseMessage);
@@ -266,10 +274,14 @@ window.CurrentWeatherScroll = {
addScreen,
reset,
start,
screenCount,
atDefault,
};
export {
addScreen,
reset,
start,
screenCount,
atDefault,
};

View File

@@ -0,0 +1,130 @@
import Setting from './utils/setting.mjs';
import { reset as resetScroll, addScreen as addScroll } from './currentweatherscroll.mjs';
import { json } from './utils/fetch.mjs';
let firstRun = true;
const parser = new DOMParser();
// change of enable handler
const changeEnable = (newValue) => {
let newDisplay;
if (newValue) {
// add the feed to the scroll
parseFeed(customFeed.value);
// show the string box
newDisplay = 'block';
} else {
// set scroll back to original
resetScroll();
// hide the string entry
newDisplay = 'none';
}
const stringEntry = document.getElementById('settings-customFeed-label');
if (stringEntry) {
stringEntry.style.display = newDisplay;
}
};
// parse the feed/text provided
const parseFeed = (textInput) => {
// skip getting the feed on first run
if (firstRun) return;
// test validity
if (textInput === undefined || textInput === '') {
resetScroll();
}
// test for url
if (textInput.match(/https?:\/\//)) {
getFeed(textInput);
return;
}
// add single text scroll
resetScroll();
addScroll(
() => (
{
type: 'scroll',
text: textInput,
}),
// keep the existing scroll
true,
);
};
// get the rss feed and then swap out the current weather scroll
const getFeed = async (url) => {
// get the text as a string
// it needs to be proxied, use a free service
const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`);
// this returns a data url
// a few sanity checks
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
if (rssResponse.contents.indexOf('base64') > 100) return;
// base 64 decode everything after the comma
const rss = atob(rssResponse.contents.split('base64,')[1]);
// parse the rss
const doc = parser.parseFromString(rss, 'text/xml');
// get the title
const rssTitle = doc.querySelector('channel title').textContent;
// get each item
const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent);
// reset the scroll, then add the screens
resetScroll();
titles.forEach((title) => {
// data is provided to the screen handler, so we return a function
addScroll(
() => ({
header: rssTitle,
type: 'scroll',
text: title,
}),
// false parameter does not include the default weather scrolls
false,
);
});
};
// change the feed source and re-load if necessary
const changeFeed = (newValue) => {
// first pass through won't have custom feed enable ready
if (firstRun) return;
if (customFeedEnable.value) {
parseFeed(newValue);
}
};
const customFeed = new Setting('customFeed', {
name: 'Custom RSS Feed',
defaultValue: '',
type: 'string',
changeAction: changeFeed,
placeholder: 'Text or URL',
});
const customFeedEnable = new Setting('customFeedEnable', {
name: 'Enable RSS Feed/Text',
defaultValue: false,
changeAction: changeEnable,
});
// initialize the custom feed inputs on the page
document.addEventListener('DOMContentLoaded', () => {
// add the controls to the page
const settingsSection = document.querySelector('#settings');
settingsSection.append(customFeedEnable.generate(), customFeed.generate());
// clear the first run value
firstRun = false;
// call change enable with the current value to show/hide the url box
// and make the call to get the feed if enabled
changeEnable(customFeedEnable.value);
});

View File

@@ -12,6 +12,7 @@ const DEFAULTS = {
stickyRead: false,
values: [],
visible: true,
placeholder: '',
};
class Setting {
@@ -33,6 +34,7 @@ class Setting {
this.values = options.values;
this.visible = options.visible;
this.changeAction = options.changeAction;
this.placeholder = options.placeholder;
// get value from url
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
@@ -50,6 +52,9 @@ class Setting {
// couldn't parse as a float, store as a string
urlState = urlValue;
}
if (this.type === 'string' && urlValue !== undefined) {
urlState = urlValue;
}
// get existing value if present
const storedValue = urlState ?? this.getFromLocalStorage();
@@ -62,6 +67,9 @@ class Setting {
case 'select':
this.selectChange({ target: { value: this.myValue } });
break;
case 'string':
this.stringChange({ target: { value: this.myValue } });
break;
case 'checkbox':
default:
this.checkboxChange({ target: { checked: this.myValue } });
@@ -126,6 +134,34 @@ class Setting {
return label;
}
generateString() {
// create a string input and accompanying set button
const label = document.createElement('label');
label.for = `settings-${this.shortName}-string`;
label.id = `settings-${this.shortName}-label`;
// text input box
const textInput = document.createElement('input');
textInput.type = 'text';
textInput.value = this.myValue;
textInput.id = `settings-${this.shortName}-string`;
textInput.name = `settings-${this.shortName}-string`;
textInput.placeholder = this.placeholder;
// set button
const setButton = document.createElement('input');
setButton.type = 'button';
setButton.value = 'Set';
setButton.id = `settings-${this.shortName}-button`;
setButton.name = `settings-${this.shortName}-button`;
setButton.addEventListener('click', () => {
this.stringChange({ target: { value: textInput.value } });
});
// assemble
label.append(textInput, setButton);
this.element = label;
return label;
}
checkboxChange(e) {
// update the state
this.myValue = e.target.checked;
@@ -148,6 +184,15 @@ class Setting {
this.changeAction(this.myValue);
}
stringChange(e) {
// update the value
this.myValue = e.target.value;
this.storeToLocalStorage(this.myValue);
// call the change action
this.changeAction(this.myValue);
}
storeToLocalStorage(value) {
if (!this.sticky) return;
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
@@ -179,8 +224,8 @@ class Setting {
switch (this.type) {
case 'boolean':
case 'checkbox':
return storedValue;
case 'select':
case 'string':
return storedValue;
default:
return null;
@@ -231,6 +276,8 @@ class Setting {
switch (this.type) {
case 'select':
return this.generateSelect();
case 'string':
return this.generateString();
case 'checkbox':
default:
return this.generateCheckbox();

View File

@@ -57,6 +57,7 @@
<script type="module" src="scripts/modules/radar.mjs"></script>
<script type="module" src="scripts/modules/settings.mjs"></script>
<script type="module" src="scripts/modules/media.mjs"></script>
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
<script type="module" src="scripts/index.mjs"></script>
<% } %>