mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Merge remote-tracking branch 'upstream/main' into modernization-and-refactor
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
130
server/scripts/modules/custom-rss-feed.mjs
Normal file
130
server/scripts/modules/custom-rss-feed.mjs
Normal 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);
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user