diff --git a/.eslintrc.json b/.eslintrc.json index 637b2eb7..751bf56d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,7 @@ }, "parserOptions": { "sourceType": "module", - "ecmaVersion": 2017, + "ecmaVersion": 2018, "ecmaFeatures": { "globalReturn": true } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d3912283..c93daa37 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -32,7 +32,7 @@ When submitting a new issue, please supply the following information: **Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX). -**Node Version**: Make sure it's version 12 or later (recommended is 14). +**Node Version**: Make sure it's version 14 or later (recommended is 16). **MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index fd922dad..b441c20a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -33,7 +33,7 @@ When submitting a new issue, please supply the following information: **Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX). -**Node Version**: Make sure it's version 12 or later (recommended is 14). +**Node Version**: Make sure it's version 14 or later (recommended is 16). **MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file. diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index 9e1047b3..cdd86e6a 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -15,14 +15,15 @@ jobs: timeout-minutes: 30 strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [14.x, 16.x, 17.x] steps: - name: Checkout code uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} + cache: "npm" - name: Install dependencies and run tests run: | Xvfb :99 -screen 0 1024x768x16 & diff --git a/.github/workflows/codecov-test-suites.yml b/.github/workflows/codecov-test-suites.yml index 8c2be8da..d080a18b 100644 --- a/.github/workflows/codecov-test-suites.yml +++ b/.github/workflows/codecov-test-suites.yml @@ -23,7 +23,7 @@ jobs: touch css/custom.css npm run test:coverage - name: Upload coverage results to codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 with: - file: ./coverage/lcov.info + files: ./coverage/lcov.info fail_ci_if_error: true diff --git a/.github/workflows/enforce-changelog.yml b/.github/workflows/enforce-changelog.yml index e1c2856d..07732cd7 100644 --- a/.github/workflows/enforce-changelog.yml +++ b/.github/workflows/enforce-changelog.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Enforce changelog️ - uses: dangoslen/changelog-enforcer@v1.6.1 + uses: dangoslen/changelog-enforcer@v2 with: changeLogPath: "CHANGELOG.md" skipLabels: "Skip Changelog" diff --git a/README.md b/README.md index 8a243f89..6fe93019 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ ![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png)

- Dependency Status - devDependency Status - CLI Best Practices - CodeCov Status - License - Tests + + License + + GitHub Actions + Build Status + + CodeCov Status + + + +

**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors). diff --git a/installers/mm.sh b/installers/mm.sh index cbbadd24..d56ffca3 100755 --- a/installers/mm.sh +++ b/installers/mm.sh @@ -1,3 +1,4 @@ +#!/bin/bash # This file is still here to keep PM2 working on older installations. cd ~/MagicMirror DISPLAY=:0 npm start diff --git a/modules/default/alert/alert.js b/modules/default/alert/alert.js index 2e471ac1..0ed34fb0 100644 --- a/modules/default/alert/alert.js +++ b/modules/default/alert/alert.js @@ -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 += "" + message.title + ""; + + 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 += "
"; + }, + + notificationReceived(notification, payload, sender) { + if (notification === "SHOW_ALERT") { + if (payload.type === "notification") { + this.showNotification(payload); + } else { + this.showAlert(payload, sender); } - msg += "" + message.message + ""; + } 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 = "
"; - } else if (typeof params.imageUrl === "undefined") { - image = "
"; - } - //Create overlay - const overlay = document.createElement("div"); - overlay.id = "overlay"; - overlay.innerHTML += '
'; - 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 += "" + params.title + ""; - } - if (params.message) { - if (message !== "") { - message += "
"; - } - - message += "" + params.message + ""; + // 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"); + } } }); diff --git a/modules/default/alert/styles/center.css b/modules/default/alert/styles/center.css new file mode 100644 index 00000000..4e8f5e1d --- /dev/null +++ b/modules/default/alert/styles/center.css @@ -0,0 +1,5 @@ +.ns-box { + margin-left: auto; + margin-right: auto; + text-align: center; +} diff --git a/modules/default/alert/styles/left.css b/modules/default/alert/styles/left.css new file mode 100644 index 00000000..86d2746c --- /dev/null +++ b/modules/default/alert/styles/left.css @@ -0,0 +1,4 @@ +.ns-box { + margin-right: auto; + text-align: left; +} diff --git a/modules/default/alert/notificationFx.css b/modules/default/alert/styles/notificationFx.css similarity index 99% rename from modules/default/alert/notificationFx.css rename to modules/default/alert/styles/notificationFx.css index 39faacf7..8e033e0d 100644 --- a/modules/default/alert/notificationFx.css +++ b/modules/default/alert/styles/notificationFx.css @@ -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, diff --git a/modules/default/alert/styles/right.css b/modules/default/alert/styles/right.css new file mode 100644 index 00000000..add9b6f1 --- /dev/null +++ b/modules/default/alert/styles/right.css @@ -0,0 +1,4 @@ +.ns-box { + margin-left: auto; + text-align: right; +} diff --git a/modules/default/alert/templates/alert.njk b/modules/default/alert/templates/alert.njk new file mode 100644 index 00000000..5592ea35 --- /dev/null +++ b/modules/default/alert/templates/alert.njk @@ -0,0 +1,18 @@ +{% if imageUrl or imageFA %} + {% set imageHeight = imageHeight if imageHeight else "80px" %} + {% if imageUrl %} + + {% else %} + + {% endif %} +
+{% endif %} +{% if title %} + {{ title }} +{% endif %} +{% if message %} + {% if title %} +
+ {% endif %} + {{ message }} +{% endif %} diff --git a/modules/default/alert/templates/notification.njk b/modules/default/alert/templates/notification.njk new file mode 100644 index 00000000..1d67bcda --- /dev/null +++ b/modules/default/alert/templates/notification.njk @@ -0,0 +1,9 @@ +{% if title %} + {{ title }} +{% endif %} +{% if message %} + {% if title %} +
+ {% endif %} + {{ message }} +{% endif %} diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 7525e3a8..c13196f1 100755 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -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()); } diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index 805e080b..16f1db90 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -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) { diff --git a/modules/default/newsfeed/newsfeed.js b/modules/default/newsfeed/newsfeed.js index 7e21f8a8..0b781964 100644 --- a/modules/default/newsfeed/newsfeed.js +++ b/modules/default/newsfeed/newsfeed.js @@ -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); diff --git a/modules/default/newsfeed/newsfeed.njk b/modules/default/newsfeed/newsfeed.njk index 2b037dcc..04f0ec79 100644 --- a/modules/default/newsfeed/newsfeed.njk +++ b/modules/default/newsfeed/newsfeed.njk @@ -1,3 +1,11 @@ +{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %} + {% if dangerouslyDisableAutoEscaping %} + {{ text | safe}} + {% else %} + {{ text }} + {% endif %} +{% endmacro %} + {% if loaded %} {% if config.showAsList %}