mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
Thanks to: @Developer-Incoming, @eltociear, @geraki, @khassel, @KristjanESPERANTO, @MagMar94, @mixasgr, @n8many, @OWL4C, @rejas, @savvadam, @sdetweil. > ⚠️ This release needs nodejs version `v22.14.0 or higher` ### Added - Add CSS support to the digital clock hour/minute/second through the use of the classes `clock-hour-digital`, `clock-minute-digital`, and `clock-second-digital`. - Add Arabic (#3719) and Esperanto translation. - Mark option `secondsColor` as deprecated in clock module. - Add Greek translation to Alerts module. - [newsfeed] Add specific ignoreOlderThan value (override) per feed (#3360) - [weather] Added option Humidity to hourly View - [weather] Added option to hide hourly entries that are Zero, hiding the entire column if empty. - [updatenotification] Added option to iterate over modules directory instead using modules defined in `config.js` (#3739) ### Changed - [core] starting clientonly now checks for needed env var `WAYLAND_DISPLAY` or `DISPLAY` and starts electron with needed parameters (if both are set wayland is used) (#3677) - [core] Optimize systeminformation calls and output (#3689) - [core] Add issue templates for feature requests and bug reports (#3695) - [core] Adapt `start:x11:dev` script - [weather/yr] The Yr weather provider now enforces a minimum `updateInterval` of 600 000 ms (10 minutes) to comply with the terms of service. If a lower value is set, it will be automatically increased to this minimum. - [weather/weatherflow] Fixed icons and added hourly support as well as UV, precipitation, and location name support. - [workflow] Run `sudo apt-get update` before installing packages to avoid install errors - [workflow] Exclude issues with label `ready (coming with next release)` from stale job ### Removed ### Updated - [core] Update requirements and dependencies including electron to v35 and formatting (#3593, #3693, #3717) - [core] Update prettier, ESLint and simplify config - Update Greek translation ### Fixed - [calendar] Fix clipping events being broadcast (#3678) - [tests] Fix Electron tests by running them under new github image ubuntu-24.04, replace xserver with labwc, running under xserver and labwc depending on env variable WAYLAND_DISPLAY is set (#3676) - [calendar] Fix arrayed symbols, #3267, again, add testcase, add testcase for #3678 - [weather] Fix wrong weatherCondition name in openmeteo provider which lead to n/a icon (#3691) - [core] Fix wrong port in log message when starting server only (#3696) - [calendar] Fix NewYork event processed on system in Central timezone shows wrong time #3701 - [weather/yr] The Yr weather provider is now able to recover from bad API responses instead of freezing (#3296) - [compliments] Fix evening events being shown during the day (#3727) - [weather] Fixed minor spacing issues when using UV Index in Hourly - [workflow] Fix command to run spellcheck --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com> Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com> Co-authored-by: Jason Stieber <jrstieber@gmail.com> Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com> Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com> Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com> Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com> Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com> Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com> Co-authored-by: Pedro Lamas <pedrolamas@gmail.com> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
441 lines
13 KiB
JavaScript
441 lines
13 KiB
JavaScript
Module.register("newsfeed", {
|
|
// Default module config.
|
|
defaults: {
|
|
feeds: [
|
|
{
|
|
title: "New York Times",
|
|
url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
|
encoding: "UTF-8" //ISO-8859-1
|
|
}
|
|
],
|
|
showAsList: false,
|
|
showSourceTitle: true,
|
|
showPublishDate: true,
|
|
broadcastNewsFeeds: true,
|
|
broadcastNewsUpdates: true,
|
|
showDescription: false,
|
|
showTitleAsUrl: false,
|
|
wrapTitle: true,
|
|
wrapDescription: true,
|
|
truncDescription: true,
|
|
lengthDescription: 400,
|
|
hideLoading: false,
|
|
reloadInterval: 5 * 60 * 1000, // every 5 minutes
|
|
updateInterval: 10 * 1000,
|
|
animationSpeed: 2.5 * 1000,
|
|
maxNewsItems: 0, // 0 for unlimited
|
|
ignoreOldItems: false,
|
|
ignoreOlderThan: 24 * 60 * 60 * 1000, // 1 day
|
|
removeStartTags: "",
|
|
removeEndTags: "",
|
|
startTags: [],
|
|
endTags: [],
|
|
prohibitedWords: [],
|
|
scrollLength: 500,
|
|
logFeedWarnings: false,
|
|
dangerouslyDisableAutoEscaping: false
|
|
},
|
|
|
|
getUrlPrefix (item) {
|
|
if (item.useCorsProxy) {
|
|
return `${location.protocol}//${location.host}${config.basePath}cors?url=`;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
// Define required scripts.
|
|
getScripts () {
|
|
return ["moment.js"];
|
|
},
|
|
|
|
//Define required styles.
|
|
getStyles () {
|
|
return ["newsfeed.css"];
|
|
},
|
|
|
|
// Define required translations.
|
|
getTranslations () {
|
|
// The translations for the default modules are defined in the core translation files.
|
|
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
|
// If you're trying to build your own module including translations, check out the documentation.
|
|
return false;
|
|
},
|
|
|
|
// Define start sequence.
|
|
start () {
|
|
Log.info(`Starting module: ${this.name}`);
|
|
|
|
// Set locale.
|
|
moment.locale(config.language);
|
|
|
|
this.newsItems = [];
|
|
this.loaded = false;
|
|
this.error = null;
|
|
this.activeItem = 0;
|
|
this.scrollPosition = 0;
|
|
|
|
this.registerFeeds();
|
|
|
|
this.isShowingDescription = this.config.showDescription;
|
|
},
|
|
|
|
// Override socket notification handler.
|
|
socketNotificationReceived (notification, payload) {
|
|
if (notification === "NEWS_ITEMS") {
|
|
this.generateFeed(payload);
|
|
|
|
if (!this.loaded) {
|
|
if (this.config.hideLoading) {
|
|
this.show();
|
|
}
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
|
|
this.loaded = true;
|
|
this.error = null;
|
|
} else if (notification === "NEWSFEED_ERROR") {
|
|
this.error = this.translate(payload.error_type);
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
//Override fetching of template name
|
|
getTemplate () {
|
|
if (this.config.feedUrl) {
|
|
return "oldconfig.njk";
|
|
} else if (this.config.showFullArticle) {
|
|
return "fullarticle.njk";
|
|
}
|
|
return "newsfeed.njk";
|
|
},
|
|
|
|
//Override template data and return whats used for the current template
|
|
getTemplateData () {
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.activeItemCount = this.newsItems.length;
|
|
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
|
if (this.config.showFullArticle) {
|
|
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
|
|
return {
|
|
url: this.getActiveItemURL()
|
|
};
|
|
}
|
|
if (this.error) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
error: this.error
|
|
};
|
|
}
|
|
if (this.newsItems.length === 0) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
empty: true
|
|
};
|
|
}
|
|
|
|
const item = this.newsItems[this.activeItem];
|
|
this.activeItemHash = item.hash;
|
|
|
|
const items = this.newsItems.map(function (item) {
|
|
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
|
return item;
|
|
});
|
|
|
|
return {
|
|
loaded: true,
|
|
config: this.config,
|
|
sourceTitle: item.sourceTitle,
|
|
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
|
title: item.title,
|
|
url: this.getActiveItemURL(),
|
|
description: item.description,
|
|
items: items
|
|
};
|
|
},
|
|
|
|
getActiveItemURL () {
|
|
const item = this.newsItems[this.activeItem];
|
|
if (item) {
|
|
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Registers the feeds to be used by the backend.
|
|
*/
|
|
registerFeeds () {
|
|
for (let feed of this.config.feeds) {
|
|
this.sendSocketNotification("ADD_FEED", {
|
|
feed: feed,
|
|
config: this.config
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets a feed property by name
|
|
* @param {object} feed A feed object.
|
|
* @param {string} property The name of the property.
|
|
*/
|
|
getFeedProperty (feed, property) {
|
|
let res = this.config[property];
|
|
const f = this.config.feeds.find((feedItem) => feedItem.url === feed);
|
|
if (f && f[property]) res = f[property];
|
|
return res;
|
|
},
|
|
|
|
/**
|
|
* Generate an ordered list of items for this configured module.
|
|
* @param {object} feeds An object with feeds returned by the node helper.
|
|
*/
|
|
generateFeed (feeds) {
|
|
let newsItems = [];
|
|
for (let feed in feeds) {
|
|
const feedItems = feeds[feed];
|
|
if (this.subscribedToFeed(feed)) {
|
|
for (let item of feedItems) {
|
|
item.sourceTitle = this.titleForFeed(feed);
|
|
if (!(this.getFeedProperty(feed, "ignoreOldItems") && Date.now() - new Date(item.pubdate) > this.getFeedProperty(feed, "ignoreOlderThan"))) {
|
|
newsItems.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
newsItems.sort(function (a, b) {
|
|
const dateA = new Date(a.pubdate);
|
|
const dateB = new Date(b.pubdate);
|
|
return dateB - dateA;
|
|
});
|
|
|
|
if (this.config.maxNewsItems > 0) {
|
|
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
|
}
|
|
|
|
if (this.config.prohibitedWords.length > 0) {
|
|
newsItems = newsItems.filter(function (item) {
|
|
for (let word of this.config.prohibitedWords) {
|
|
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}, this);
|
|
}
|
|
newsItems.forEach((item) => {
|
|
//Remove selected tags from the beginning of rss feed items (title or description)
|
|
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.title.slice(0, startTag.length) === startTag) {
|
|
item.title = item.title.slice(startTag.length, item.title.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
|
if (this.isShowingDescription) {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.description.slice(0, startTag.length) === startTag) {
|
|
item.description = item.description.slice(startTag.length, item.description.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Remove selected tags from the end of rss feed items (title or description)
|
|
if (this.config.removeEndTags) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.title.slice(-endTag.length) === endTag) {
|
|
item.title = item.title.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
|
|
if (this.isShowingDescription) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.description.slice(-endTag.length) === endTag) {
|
|
item.description = item.description.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// get updated news items and broadcast them
|
|
const updatedItems = [];
|
|
newsItems.forEach((value) => {
|
|
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
|
|
// Add item to updated items list
|
|
updatedItems.push(value);
|
|
}
|
|
});
|
|
|
|
// check if updated items exist, if so and if we should broadcast these updates, then lets do so
|
|
if (this.config.broadcastNewsUpdates && updatedItems.length > 0) {
|
|
this.sendNotification("NEWS_FEED_UPDATE", { items: updatedItems });
|
|
}
|
|
|
|
this.newsItems = newsItems;
|
|
},
|
|
|
|
/**
|
|
* Check if this module is configured to show this feed.
|
|
* @param {string} feedUrl Url of the feed to check.
|
|
* @returns {boolean} True if it is subscribed, false otherwise
|
|
*/
|
|
subscribedToFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns title for the specific feed url.
|
|
* @param {string} feedUrl Url of the feed
|
|
* @returns {string} The title of the feed
|
|
*/
|
|
titleForFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return feed.title || "";
|
|
}
|
|
}
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Schedule visual update.
|
|
*/
|
|
scheduleUpdateInterval () {
|
|
this.updateDom(this.config.animationSpeed);
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
|
|
// #2638 Clear timer if it already exists
|
|
if (this.timer) clearInterval(this.timer);
|
|
|
|
this.timer = setInterval(() => {
|
|
|
|
/*
|
|
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
|
|
* (Animating from a headline to itself is unsightly.)
|
|
* Cases:
|
|
*
|
|
* Number of items | Number of items | Display
|
|
* at last update | right now | Behaviour
|
|
* ----------------------------------------------------
|
|
* 0 | 0 | do not update
|
|
* 0 | >0 | update
|
|
* 1 | 0 or >1 | update
|
|
* 1 | 1 | update only if item details (hash value) changed
|
|
* >1 | any | update
|
|
*
|
|
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
|
|
*/
|
|
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
|
|
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
|
|
this.updateDom(this.config.animationSpeed);
|
|
}
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
}, this.config.updateInterval);
|
|
},
|
|
|
|
resetDescrOrFullArticleAndTimer () {
|
|
this.isShowingDescription = this.config.showDescription;
|
|
this.config.showFullArticle = false;
|
|
this.scrollPosition = 0;
|
|
// reset bottom bar alignment
|
|
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
|
|
if (!this.timer) {
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
notificationReceived (notification, payload, sender) {
|
|
const before = this.activeItem;
|
|
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
|
this.hide();
|
|
} else if (notification === "ARTICLE_NEXT") {
|
|
this.activeItem++;
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_PREVIOUS") {
|
|
this.activeItem--;
|
|
if (this.activeItem < 0) {
|
|
this.activeItem = this.newsItems.length - 1;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
}
|
|
// if "more details" is received the first time: show article summary, on second time show full article
|
|
else if (notification === "ARTICLE_MORE_DETAILS") {
|
|
// full article is already showing, so scrolling down
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition += this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling down`);
|
|
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_SCROLL_UP") {
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition -= this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling up`);
|
|
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
|
}
|
|
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - showing only article titles again`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
|
if (this.config.showFullArticle) {
|
|
this.activeItem++;
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_INFO_REQUEST") {
|
|
this.sendNotification("ARTICLE_INFO_RESPONSE", {
|
|
title: this.newsItems[this.activeItem].title,
|
|
source: this.newsItems[this.activeItem].sourceTitle,
|
|
date: this.newsItems[this.activeItem].pubdate,
|
|
desc: this.newsItems[this.activeItem].description,
|
|
url: this.getActiveItemURL()
|
|
});
|
|
}
|
|
},
|
|
|
|
showFullArticle () {
|
|
this.isShowingDescription = !this.isShowingDescription;
|
|
this.config.showFullArticle = !this.isShowingDescription;
|
|
// make bottom bar align to top to allow scrolling
|
|
if (this.config.showFullArticle === true) {
|
|
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
|
|
}
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
|
this.updateDom(100);
|
|
}
|
|
});
|