mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-21 12:55:22 +00:00
Merge branch 'develop' into calfix
This commit is contained in:
@@ -7,161 +7,140 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
defaults: {
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
effect: "slide",
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly",
|
||||
//time a notification is displayed in seconds
|
||||
display_time: 3500,
|
||||
//Position
|
||||
effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
display_time: 3500, // time a notification is displayed in seconds
|
||||
position: "center",
|
||||
//shown at startup
|
||||
welcome_message: false
|
||||
welcome_message: false // shown at startup
|
||||
},
|
||||
getScripts: function () {
|
||||
|
||||
getScripts() {
|
||||
return ["notificationFx.js"];
|
||||
},
|
||||
getStyles: function () {
|
||||
return ["notificationFx.css", "font-awesome.css"];
|
||||
|
||||
getStyles() {
|
||||
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
|
||||
getTranslations() {
|
||||
return {
|
||||
en: "translations/en.json",
|
||||
bg: "translations/bg.json",
|
||||
da: "translations/da.json",
|
||||
de: "translations/de.json",
|
||||
nl: "translations/nl.json"
|
||||
en: "translations/en.json",
|
||||
es: "translations/es.json",
|
||||
fr: "translations/fr.json",
|
||||
hu: "translations/hu.json",
|
||||
nl: "translations/nl.json",
|
||||
ru: "translations/ru.json"
|
||||
};
|
||||
},
|
||||
show_notification: function (message) {
|
||||
|
||||
getTemplate(type) {
|
||||
return `templates/${type}.njk`;
|
||||
},
|
||||
|
||||
start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.effect === "slide") {
|
||||
this.config.effect = this.config.effect + "-" + this.config.position;
|
||||
this.config.effect = `${this.config.effect}-${this.config.position}`;
|
||||
}
|
||||
let msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
|
||||
if (this.config.welcome_message) {
|
||||
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
|
||||
this.showNotification({ title: this.translate("sysTitle"), message });
|
||||
}
|
||||
if (message.message) {
|
||||
if (msg !== "") {
|
||||
msg += "<br />";
|
||||
},
|
||||
|
||||
notificationReceived(notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (payload.type === "notification") {
|
||||
this.showNotification(payload);
|
||||
} else {
|
||||
this.showAlert(payload, sender);
|
||||
}
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hideAlert(sender);
|
||||
}
|
||||
},
|
||||
|
||||
async showNotification(notification) {
|
||||
const message = await this.renderMessage("notification", notification);
|
||||
|
||||
new NotificationFx({
|
||||
message: msg,
|
||||
message,
|
||||
layout: "growl",
|
||||
effect: this.config.effect,
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
ttl: notification.timer || this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function (params, sender) {
|
||||
let image = "";
|
||||
//Set standard params if not provided by module
|
||||
if (typeof params.timer === "undefined") {
|
||||
params.timer = null;
|
||||
}
|
||||
if (typeof params.imageHeight === "undefined") {
|
||||
params.imageHeight = "80px";
|
||||
}
|
||||
if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") {
|
||||
params.imageUrl = null;
|
||||
} else if (typeof params.imageFA === "undefined") {
|
||||
image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />";
|
||||
} else if (typeof params.imageUrl === "undefined") {
|
||||
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />";
|
||||
}
|
||||
//Create overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "overlay";
|
||||
overlay.innerHTML += '<div class="black_overlay"></div>';
|
||||
document.body.insertBefore(overlay, document.body.firstChild);
|
||||
|
||||
//If module already has an open alert close it
|
||||
async showAlert(alert, sender) {
|
||||
// If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender, false);
|
||||
this.hideAlert(sender, false);
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
let message = "";
|
||||
if (params.title) {
|
||||
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||
}
|
||||
if (params.message) {
|
||||
if (message !== "") {
|
||||
message += "<br />";
|
||||
}
|
||||
|
||||
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||
// Add overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(true);
|
||||
}
|
||||
|
||||
//Store alert in this.alerts
|
||||
const message = await this.renderMessage("alert", alert);
|
||||
|
||||
// Store alert in this.alerts
|
||||
this.alerts[sender.name] = new NotificationFx({
|
||||
message: image + message,
|
||||
message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
ttl: alert.timer,
|
||||
onClose: () => this.hideAlert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
// Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
// Add timer to dismiss alert and overlay
|
||||
if (alert.timer) {
|
||||
setTimeout(() => {
|
||||
this.hide_alert(sender);
|
||||
}, params.timer);
|
||||
this.hideAlert(sender);
|
||||
}, alert.timer);
|
||||
}
|
||||
},
|
||||
hide_alert: function (sender, close = true) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
|
||||
hideAlert(sender, close = true) {
|
||||
// Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
},
|
||||
setPosition: function (pos) {
|
||||
//Add css to body depending on the set position for notifications
|
||||
const sheet = document.createElement("style");
|
||||
if (pos === "center") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";
|
||||
}
|
||||
if (pos === "right") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";
|
||||
}
|
||||
if (pos === "left") {
|
||||
sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";
|
||||
}
|
||||
document.body.appendChild(sheet);
|
||||
},
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") {
|
||||
payload.type = "alert";
|
||||
}
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type === "notification") {
|
||||
this.show_notification(payload);
|
||||
}
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hide_alert(sender);
|
||||
}
|
||||
},
|
||||
start: function () {
|
||||
this.alerts = {};
|
||||
this.setPosition(this.config.position);
|
||||
if (this.config.welcome_message) {
|
||||
if (this.config.welcome_message === true) {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
|
||||
} else {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
|
||||
delete this.alerts[sender.name];
|
||||
// Remove overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(false);
|
||||
}
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
renderMessage(type, data) {
|
||||
return new Promise((resolve) => {
|
||||
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
|
||||
if (err) {
|
||||
Log.error("Failed to render alert", err);
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleBlur(add = false) {
|
||||
const method = add ? "add" : "remove";
|
||||
const modules = document.querySelectorAll(".module");
|
||||
for (const module of modules) {
|
||||
module.classList[method]("alert-blur");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
5
modules/default/alert/styles/center.css
Normal file
5
modules/default/alert/styles/center.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
4
modules/default/alert/styles/left.css
Normal file
4
modules/default/alert/styles/left.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
@@ -39,12 +39,8 @@
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.black_overlay {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.alert-blur {
|
||||
filter: blur(2px) brightness(50%);
|
||||
}
|
||||
|
||||
[class^="ns-effect-"].ns-growl.ns-hide,
|
4
modules/default/alert/styles/right.css
Normal file
4
modules/default/alert/styles/right.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
18
modules/default/alert/templates/alert.njk
Normal file
18
modules/default/alert/templates/alert.njk
Normal file
@@ -0,0 +1,18 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px;"/>
|
||||
{% else %}
|
||||
<span class="bright fa fa-{{ imageFA }}" style='margin-bottom: 10px; font-size: {{ imageHeight }};'/></span>
|
||||
{% endif %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% endif %}
|
9
modules/default/alert/templates/notification.njk
Normal file
9
modules/default/alert/templates/notification.njk
Normal file
@@ -0,0 +1,9 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% endif %}
|
@@ -368,9 +368,9 @@ Module.register("calendar", {
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime) {
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
@@ -378,11 +378,22 @@ Module.register("calendar", {
|
||||
sameDay: "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd",
|
||||
sameElse: this.config.dateFormat
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
);
|
||||
}
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
} else if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
|
@@ -332,15 +332,18 @@ const CalendarUtils = {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
|
||||
//if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
//}
|
||||
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
|
@@ -37,7 +37,8 @@ Module.register("newsfeed", {
|
||||
endTags: [],
|
||||
prohibitedWords: [],
|
||||
scrollLength: 500,
|
||||
logFeedWarnings: false
|
||||
logFeedWarnings: false,
|
||||
dangerouslyDisableAutoEscaping: false
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -121,7 +122,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
return {
|
||||
loaded: false
|
||||
empty: true
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
@@ -184,6 +185,7 @@ Module.register("newsfeed", {
|
||||
const dateB = new Date(b.pubdate);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
if (this.config.maxNewsItems > 0) {
|
||||
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
||||
}
|
||||
@@ -219,7 +221,6 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
//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) {
|
||||
@@ -295,6 +296,9 @@ Module.register("newsfeed", {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
|
||||
// #2638 Clear timer if it already exists
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
@@ -1,3 +1,11 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{{ text | safe}}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
@@ -14,14 +22,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ item.title }}
|
||||
{{ escapeText(item.title, config.dangerouslyDisableAutoEscaping) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ item.description | truncate(config.lengthDescription) }}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ item.description }}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -33,7 +41,7 @@
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
@@ -41,19 +49,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ title }}
|
||||
{{ escapeText(title, config.dangerouslyDisableAutoEscaping) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">
|
||||
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
|
||||
</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
|
@@ -79,7 +79,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version,
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
|
@@ -4,48 +4,53 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
|
||||
class gitHelper {
|
||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
|
||||
class GitHelper {
|
||||
constructor() {
|
||||
this.gitRepos = [];
|
||||
this.baseDir = path.normalize(__dirname + "/../../../");
|
||||
}
|
||||
|
||||
getRefRegex(branch) {
|
||||
return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g");
|
||||
return new RegExp(`s*([a-z,0-9]+[.][.][a-z,0-9]+) ${branch}`, "g");
|
||||
}
|
||||
|
||||
async execShell(command) {
|
||||
let res = { stdout: "", stderr: "" };
|
||||
const { stdout, stderr } = await exec(command);
|
||||
const { stdout = "", stderr = "" } = await exec(command);
|
||||
|
||||
res.stdout = stdout;
|
||||
res.stderr = stderr;
|
||||
return res;
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
async isGitRepo(moduleFolder) {
|
||||
let res = await this.execShell("cd " + moduleFolder + " && git remote -v");
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to fetch git data for " + moduleFolder + ": " + res.stderr);
|
||||
const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to fetch git data for ${moduleFolder}: ${stderr}`);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
add(moduleName) {
|
||||
let moduleFolder = this.baseDir;
|
||||
async add(moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
|
||||
if (moduleName !== "default") {
|
||||
moduleFolder = moduleFolder + "modules/" + moduleName;
|
||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
Log.info(`Checking git for module: ${moduleName}`);
|
||||
// Throws error if file doesn't exist
|
||||
fs.statSync(path.join(moduleFolder, ".git"));
|
||||
|
||||
// Fetch the git or throw error if no remotes
|
||||
if (this.isGitRepo(moduleFolder)) {
|
||||
const isGitRepo = await this.isGitRepo(moduleFolder);
|
||||
|
||||
if (isGitRepo) {
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
|
||||
this.gitRepos.push({ module: moduleName, folder: moduleFolder });
|
||||
}
|
||||
} catch (err) {
|
||||
// Error when directory .git doesn't exist or doesn't have any remotes
|
||||
@@ -56,38 +61,34 @@ class gitHelper {
|
||||
async getStatusInfo(repo) {
|
||||
let gitInfo = {
|
||||
module: repo.module,
|
||||
// commits behind:
|
||||
behind: 0,
|
||||
// branch name:
|
||||
current: "",
|
||||
// current hash:
|
||||
hash: "",
|
||||
// remote branch:
|
||||
tracking: "",
|
||||
behind: 0, // commits behind
|
||||
current: "", // branch name
|
||||
hash: "", // current hash
|
||||
tracking: "", // remote branch
|
||||
isBehindInStatus: false
|
||||
};
|
||||
let res;
|
||||
|
||||
if (repo.module === "default") {
|
||||
// the hash is only needed for the mm repo
|
||||
res = await this.execShell("cd " + repo.folder + " && git rev-parse HEAD");
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to get current commit hash for " + repo.module + ": " + res.stderr);
|
||||
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to get current commit hash for ${repo.module}: ${stderr}`);
|
||||
}
|
||||
gitInfo.hash = res.stdout;
|
||||
|
||||
gitInfo.hash = stdout;
|
||||
}
|
||||
if (repo.res) {
|
||||
// mocking
|
||||
res = repo.res;
|
||||
} else {
|
||||
res = await this.execShell("cd " + repo.folder + " && git status -sb");
|
||||
}
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to get git status for " + repo.module + ": " + res.stderr);
|
||||
|
||||
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git status -sb`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to get git status for ${repo.module}: ${stderr}`);
|
||||
// exit without git status info
|
||||
return;
|
||||
}
|
||||
|
||||
// only the first line of stdout is evaluated
|
||||
let status = res.stdout.split("\n")[0];
|
||||
let status = stdout.split("\n")[0];
|
||||
// examples for status:
|
||||
// ## develop...origin/develop
|
||||
// ## master...origin/master [behind 8]
|
||||
@@ -101,72 +102,63 @@ class gitHelper {
|
||||
// [ 'origin/develop' ]
|
||||
// [ 'origin/master', '[behind', '8]' ]
|
||||
gitInfo.tracking = status[0].trim();
|
||||
|
||||
if (status[2]) {
|
||||
// git fetch was already called before so `git status -sb` delivers already the behind number
|
||||
gitInfo.behind = parseInt(status[2].substring(0, status[2].length - 1));
|
||||
gitInfo.isBehindInStatus = true;
|
||||
}
|
||||
|
||||
return gitInfo;
|
||||
}
|
||||
|
||||
async getRepoInfo(repo) {
|
||||
let gitInfo;
|
||||
if (repo.gitInfo) {
|
||||
// mocking
|
||||
gitInfo = repo.gitInfo;
|
||||
} else {
|
||||
gitInfo = await this.getStatusInfo(repo);
|
||||
}
|
||||
const gitInfo = await this.getStatusInfo(repo);
|
||||
|
||||
if (!gitInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitInfo.isBehindInStatus) {
|
||||
return gitInfo;
|
||||
}
|
||||
let res;
|
||||
if (repo.res) {
|
||||
// mocking
|
||||
res = repo.res;
|
||||
} else {
|
||||
res = await this.execShell("cd " + repo.folder + " && git fetch --dry-run");
|
||||
}
|
||||
|
||||
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`);
|
||||
|
||||
// example output:
|
||||
// From https://github.com/MichMich/MagicMirror
|
||||
// e40ddd4..06389e3 develop -> origin/develop
|
||||
// here the result is in stderr (this is a git default, don't ask why ...)
|
||||
const matches = res.stderr.match(this.getRefRegex(gitInfo.current));
|
||||
const matches = stderr.match(this.getRefRegex(gitInfo.current));
|
||||
|
||||
if (!matches || !matches[0]) {
|
||||
// no refs found, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// get behind with refs
|
||||
try {
|
||||
res = await this.execShell("cd " + repo.folder + " && git rev-list --ancestry-path --count " + matches[0]);
|
||||
gitInfo.behind = parseInt(res.stdout);
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`);
|
||||
gitInfo.behind = parseInt(stdout);
|
||||
|
||||
return gitInfo;
|
||||
} catch (err) {
|
||||
Log.error("Failed to get git revisions for " + repo.module + ": " + err);
|
||||
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
const gitResultList = [];
|
||||
for (let repo of this.gitRepos) {
|
||||
const gitInfo = await this.getStatusInfo(repo);
|
||||
if (gitInfo) {
|
||||
gitResultList.unshift(gitInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return gitResultList;
|
||||
}
|
||||
|
||||
async getRepos() {
|
||||
const gitResultList = [];
|
||||
for (let repo of this.gitRepos) {
|
||||
const gitInfo = await this.getRepoInfo(repo);
|
||||
if (gitInfo) {
|
||||
gitResultList.unshift(gitInfo);
|
||||
|
||||
for (const repo of this.gitRepos) {
|
||||
try {
|
||||
const gitInfo = await this.getRepoInfo(repo);
|
||||
|
||||
if (gitInfo) {
|
||||
gitResultList.push(gitInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,4 +166,4 @@ class gitHelper {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.gitHelper = gitHelper;
|
||||
module.exports = GitHelper;
|
||||
|
@@ -1,70 +1,66 @@
|
||||
const GitHelper = require(__dirname + "/git_helper.js");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const GitHelper = require("./git_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
config: {},
|
||||
|
||||
updateTimer: null,
|
||||
updateProcessStarted: false,
|
||||
|
||||
gitHelper: new GitHelper.gitHelper(),
|
||||
gitHelper: new GitHelper(),
|
||||
|
||||
start: function () {},
|
||||
|
||||
configureModules: async function (modules) {
|
||||
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
|
||||
// others will be added in front
|
||||
// this method returns promises so we can't wait for every one to resolve before continuing
|
||||
this.gitHelper.add("default");
|
||||
|
||||
for (let moduleName in modules) {
|
||||
async configureModules(modules) {
|
||||
for (const moduleName of modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
this.gitHelper.add(moduleName);
|
||||
await this.gitHelper.add(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
await this.gitHelper.add("default");
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
async socketNotificationReceived(notification, payload) {
|
||||
if (notification === "CONFIG") {
|
||||
this.config = payload;
|
||||
} else if (notification === "MODULES") {
|
||||
// if this is the 1st time thru the update check process
|
||||
if (!this.updateProcessStarted) {
|
||||
this.updateProcessStarted = true;
|
||||
this.configureModules(payload).then(() => this.performFetch());
|
||||
await this.configureModules(payload);
|
||||
await this.performFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
performFetch: async function () {
|
||||
for (let gitInfo of await this.gitHelper.getRepos()) {
|
||||
this.sendSocketNotification("STATUS", gitInfo);
|
||||
async performFetch() {
|
||||
const repos = await this.gitHelper.getRepos();
|
||||
|
||||
for (const repo of repos) {
|
||||
this.sendSocketNotification("STATUS", repo);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
|
||||
scheduleNextFetch: function (delay) {
|
||||
if (delay < 60 * 1000) {
|
||||
delay = 60 * 1000;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
scheduleNextFetch(delay) {
|
||||
clearTimeout(this.updateTimer);
|
||||
this.updateTimer = setTimeout(function () {
|
||||
self.performFetch();
|
||||
}, delay);
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
this.performFetch();
|
||||
}, Math.max(delay, ONE_MINUTE));
|
||||
},
|
||||
|
||||
ignoreUpdateChecking: function (moduleName) {
|
||||
ignoreUpdateChecking(moduleName) {
|
||||
// Should not check for updates for default modules
|
||||
if (defaultModules.indexOf(moduleName) >= 0) {
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Should not check for updates for ignored modules
|
||||
if (this.config.ignoreModules.indexOf(moduleName) >= 0) {
|
||||
if (this.config.ignoreModules.includes(moduleName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.module.updatenotification a.difflink {
|
||||
text-decoration: none;
|
||||
}
|
@@ -5,42 +5,59 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("updatenotification", {
|
||||
// Define module defaults
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: [],
|
||||
timeout: 5000
|
||||
ignoreModules: []
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
this.addFilters();
|
||||
setInterval(() => {
|
||||
this.moduleList = {};
|
||||
this.updateDom(2);
|
||||
}, this.config.refreshInterval);
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
suspend() {
|
||||
this.suspended = true;
|
||||
},
|
||||
|
||||
resume() {
|
||||
this.suspended = false;
|
||||
this.updateDom(2);
|
||||
},
|
||||
|
||||
notificationReceived(notification) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Module.definitions);
|
||||
//this.hide(0, { lockString: this.identifier });
|
||||
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
|
||||
}
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived(notification, payload) {
|
||||
if (notification === "STATUS") {
|
||||
this.updateUI(payload);
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: function (payload) {
|
||||
getStyles() {
|
||||
return [`${this.name}.css`];
|
||||
},
|
||||
|
||||
getTemplate() {
|
||||
return `${this.name}.njk`;
|
||||
},
|
||||
|
||||
getTemplateData() {
|
||||
return { moduleList: this.moduleList, suspended: this.suspended };
|
||||
},
|
||||
|
||||
updateUI(payload) {
|
||||
if (payload && payload.behind > 0) {
|
||||
// if we haven't seen info for this module
|
||||
if (this.moduleList[payload.module] === undefined) {
|
||||
@@ -48,7 +65,6 @@ Module.register("updatenotification", {
|
||||
this.moduleList[payload.module] = payload;
|
||||
this.updateDom(2);
|
||||
}
|
||||
//self.show(1000, { lockString: self.identifier });
|
||||
} else if (payload && payload.behind === 0) {
|
||||
// if the module WAS in the list, but shouldn't be
|
||||
if (this.moduleList[payload.module] !== undefined) {
|
||||
@@ -59,62 +75,15 @@ Module.register("updatenotification", {
|
||||
}
|
||||
},
|
||||
|
||||
diffLink: function (module, text) {
|
||||
const localRef = module.hash;
|
||||
const remoteRef = module.tracking.replace(/.*\//, "");
|
||||
return '<a href="https://github.com/MichMich/MagicMirror/compare/' + localRef + "..." + remoteRef + '" ' + 'class="xsmall dimmed" ' + 'style="text-decoration: none;" ' + 'target="_blank" >' + text + "</a>";
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
const wrapper = document.createElement("div");
|
||||
if (this.suspended === false) {
|
||||
// process the hash of module info found
|
||||
for (const key of Object.keys(this.moduleList)) {
|
||||
let m = this.moduleList[key];
|
||||
|
||||
const message = document.createElement("div");
|
||||
message.className = "small bright";
|
||||
|
||||
const icon = document.createElement("i");
|
||||
icon.className = "fa fa-exclamation-circle";
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
|
||||
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
|
||||
let subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: m.behind,
|
||||
BRANCH_NAME: m.current
|
||||
});
|
||||
|
||||
const text = document.createElement("span");
|
||||
if (m.module === "default") {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||
subtextHtml = this.diffLink(m, subtextHtml);
|
||||
} else {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
|
||||
MODULE_NAME: m.module
|
||||
});
|
||||
}
|
||||
message.appendChild(text);
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
const subtext = document.createElement("div");
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
addFilters() {
|
||||
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
|
||||
if (status.module !== "default") {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
suspend: function () {
|
||||
this.suspended = true;
|
||||
},
|
||||
resume: function () {
|
||||
this.suspended = false;
|
||||
this.updateDom(2);
|
||||
const localRef = status.hash;
|
||||
const remoteRef = status.tracking.replace(/.*\//, "");
|
||||
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
15
modules/default/updatenotification/updatenotification.njk
Normal file
15
modules/default/updatenotification/updatenotification.njk
Normal file
@@ -0,0 +1,15 @@
|
||||
{% if not suspended %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "default" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
@@ -127,6 +127,8 @@ WeatherProvider.register("openweathermap", {
|
||||
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
||||
|
||||
if (this.config.windUnits === "metric") {
|
||||
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||
} else {
|
||||
|
@@ -113,20 +113,30 @@ const WeatherProvider = Class.extend({
|
||||
|
||||
// A convenience function to make requests. It returns a promise.
|
||||
fetchData: function (url, method = "GET", data = null) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
resolve(JSON.parse(this.response));
|
||||
} else {
|
||||
reject(request);
|
||||
}
|
||||
const getData = function (mockData) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (mockData) {
|
||||
let data = mockData;
|
||||
data = data.substring(1, data.length - 1);
|
||||
resolve(JSON.parse(data));
|
||||
} else {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
resolve(JSON.parse(this.response));
|
||||
} else {
|
||||
reject(request);
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return getData(this.config.mockData);
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user