mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
## [2.26.0] - 01-01-2024 Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! This release also marks the latest release by Michael Teeuw. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Added update notification updater (for 3rd party modules) - Added node 21 to the test matrix - Added transform object to calendar:customEvents - Added ESLint rules for jest (including jest/expect-expect and jest/no-done-callback) ### Removed - Removed Codecov workflow (not working anymore, other workflow required) (#3107) - Removed titleReplace from calendar, replaced + extended by customEvents (backward compatibility included) (#3249) - Removed failing unit test (#3254) - Removed some unused variables ### Updated - Update electron to v27 and update other dependencies as well as github actions - Update newsfeed: Use `html-to-text` instead of regex for transform description - Review ESLint config (#3269) - Updated dependencies - Clock module: optionally display current moon phase in addition to rise/set times - electron is now per default started without gpu, if needed it must be enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226) - Replace prettier by stylistic in ESLint config to lint JavaScript (and disable some rules for `config/config.js*` files) - Update node-ical to v0.17.1 and fix tests ### Fixed - Avoid fade out/in on updateDom when many calendars are used - Fix the option eventClass on customEvents. - Fix yr API version in locationforecast and sunrise call (#3227) - Fix cloneObject() function to respect RegExp (#3237) - Fix newsfeed module for feeds using "a10:updated" tag (#3238) - Fix issue template (#3167) - Fix #3256 filter out bad results from rrule.between - Fix calendar events sometimes not respecting deleted events (#3250) - Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550) - Fix updatanotification (update_helper.js): catch error if reponse is not an JSON format (check PM2) - Fix missing typeof in calendar module - Fix style issues after prettier update - Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday - Fix calendar config params `fetchInterval` and `excludedEvents` were never used from single calendar config (#3297) - Fix MM_PORT variable not used in electron and allow full path for MM_CONFIG_FILE variable (#3302) --------- Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: Dave Child <dave@addedbytes.com> Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com> Co-authored-by: Grena <grena@grenabox.fr> Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Patrick <psieg@users.noreply.github.com> Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com> Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com> Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com> Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com> Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com> Co-authored-by: Ismar Slomic <ismar@slomic.no> Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no> Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: bugsounet <bugsounet@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Knapoc <Knapoc@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com> Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com> Co-authored-by: Teddy <teddy.payet@gmail.com> Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com> Co-authored-by: martingron <61826403+martingron@users.noreply.github.com> Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com> Co-authored-by: kaennchenstruggle <54073894+kaennchenstruggle@users.noreply.github.com> Co-authored-by: jkriegshauser <jkriegshauser@gmail.com> Co-authored-by: Ben Nitkin <ben@nitkin.net>
739 lines
24 KiB
JavaScript
739 lines
24 KiB
JavaScript
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
|
|
|
/* MagicMirror²
|
|
* Main System
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*/
|
|
const MM = (function () {
|
|
let modules = [];
|
|
|
|
/* Private Methods */
|
|
|
|
/**
|
|
* Create dom objects for all modules that are configured for a specific position.
|
|
*/
|
|
const createDomObjects = function () {
|
|
const domCreationPromises = [];
|
|
|
|
modules.forEach(function (module) {
|
|
if (typeof module.data.position !== "string") {
|
|
return;
|
|
}
|
|
|
|
let haveAnimateIn = null;
|
|
// check if have valid animateIn in module definition (module.data.animateIn)
|
|
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn;
|
|
|
|
const wrapper = selectWrapper(module.data.position);
|
|
|
|
const dom = document.createElement("div");
|
|
dom.id = module.identifier;
|
|
dom.className = module.name;
|
|
|
|
if (typeof module.data.classes === "string") {
|
|
dom.className = `module ${dom.className} ${module.data.classes}`;
|
|
}
|
|
|
|
dom.opacity = 0;
|
|
wrapper.appendChild(dom);
|
|
|
|
const moduleHeader = document.createElement("header");
|
|
moduleHeader.innerHTML = module.getHeader();
|
|
moduleHeader.className = "module-header";
|
|
dom.appendChild(moduleHeader);
|
|
|
|
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
|
|
moduleHeader.style.display = "none;";
|
|
} else {
|
|
moduleHeader.style.display = "block;";
|
|
}
|
|
|
|
const moduleContent = document.createElement("div");
|
|
moduleContent.className = "module-content";
|
|
dom.appendChild(moduleContent);
|
|
|
|
// create the domCreationPromise with AnimateCSS (with animateIn of module definition)
|
|
// or just display it
|
|
var domCreationPromise;
|
|
if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true);
|
|
else domCreationPromise = updateDom(module, 0);
|
|
|
|
domCreationPromises.push(domCreationPromise);
|
|
domCreationPromise
|
|
.then(function () {
|
|
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
|
})
|
|
.catch(Log.error);
|
|
});
|
|
|
|
updateWrapperStates();
|
|
|
|
Promise.all(domCreationPromises).then(function () {
|
|
sendNotification("DOM_OBJECTS_CREATED");
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Select the wrapper dom object for a specific position.
|
|
* @param {string} position The name of the position.
|
|
* @returns {HTMLElement | void} the wrapper element
|
|
*/
|
|
const selectWrapper = function (position) {
|
|
const classes = position.replace("_", " ");
|
|
const parentWrapper = document.getElementsByClassName(classes);
|
|
if (parentWrapper.length > 0) {
|
|
const wrapper = parentWrapper[0].getElementsByClassName("container");
|
|
if (wrapper.length > 0) {
|
|
return wrapper[0];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Send a notification to all modules.
|
|
* @param {string} notification The identifier of the notification.
|
|
* @param {*} payload The payload of the notification.
|
|
* @param {Module} sender The module that sent the notification.
|
|
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
|
*/
|
|
const sendNotification = function (notification, payload, sender, sendTo) {
|
|
for (const m in modules) {
|
|
const module = modules[m];
|
|
if (module !== sender && (!sendTo || module === sendTo)) {
|
|
module.notificationReceived(notification, payload, sender);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the dom for a specific module.
|
|
* @param {Module} module The module that needs an update.
|
|
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
|
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
|
|
* @returns {Promise} Resolved when the dom is fully updated.
|
|
*/
|
|
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
|
|
return new Promise(function (resolve) {
|
|
let speed = updateOptions;
|
|
let animateOut = null;
|
|
let animateIn = null;
|
|
if (typeof updateOptions === "object") {
|
|
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
|
|
speed = updateOptions.options.speed;
|
|
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
|
|
if (typeof updateOptions.options.animate === "object") {
|
|
animateOut = updateOptions.options.animate.out;
|
|
animateIn = updateOptions.options.animate.in;
|
|
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
|
|
}
|
|
} else {
|
|
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
|
|
speed = 0;
|
|
}
|
|
}
|
|
|
|
const newHeader = module.getHeader();
|
|
let newContentPromise = module.getDom();
|
|
|
|
if (!(newContentPromise instanceof Promise)) {
|
|
// convert to a promise if not already one to avoid if/else's everywhere
|
|
newContentPromise = Promise.resolve(newContentPromise);
|
|
}
|
|
|
|
newContentPromise
|
|
.then(function (newContent) {
|
|
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
|
|
|
|
updatePromise.then(resolve).catch(Log.error);
|
|
})
|
|
.catch(Log.error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Update the dom with the specified content
|
|
* @param {Module} module The module that needs an update.
|
|
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
|
* @param {string} newHeader The new header that is generated.
|
|
* @param {HTMLElement} newContent The new content that is generated.
|
|
* @param {string} [animateOut] AnimateCss animation name before hidden
|
|
* @param {string} [animateIn] AnimateCss animation name on show
|
|
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
|
|
* @returns {Promise} Resolved when the module dom has been updated.
|
|
*/
|
|
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
|
|
return new Promise(function (resolve) {
|
|
if (module.hidden || !speed) {
|
|
updateModuleContent(module, newHeader, newContent);
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (!speed) {
|
|
updateModuleContent(module, newHeader, newContent);
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (createAnimatedDom && animateIn !== null) {
|
|
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
|
|
updateModuleContent(module, newHeader, newContent);
|
|
if (!module.hidden) {
|
|
showModule(module, speed, null, { animate: animateIn });
|
|
}
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
hideModule(
|
|
module,
|
|
speed / 2,
|
|
function () {
|
|
updateModuleContent(module, newHeader, newContent);
|
|
if (!module.hidden) {
|
|
showModule(module, speed / 2, null, { animate: animateIn });
|
|
}
|
|
resolve();
|
|
},
|
|
{ animate: animateOut }
|
|
);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Check if the content has changed.
|
|
* @param {Module} module The module to check.
|
|
* @param {string} newHeader The new header that is generated.
|
|
* @param {HTMLElement} newContent The new content that is generated.
|
|
* @returns {boolean} True if the module need an update, false otherwise
|
|
*/
|
|
const moduleNeedsUpdate = function (module, newHeader, newContent) {
|
|
const moduleWrapper = document.getElementById(module.identifier);
|
|
if (moduleWrapper === null) {
|
|
return false;
|
|
}
|
|
|
|
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
|
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
|
|
|
let headerNeedsUpdate = false;
|
|
let contentNeedsUpdate;
|
|
|
|
if (headerWrapper.length > 0) {
|
|
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
|
|
}
|
|
|
|
const tempContentWrapper = document.createElement("div");
|
|
tempContentWrapper.appendChild(newContent);
|
|
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
|
|
|
|
return headerNeedsUpdate || contentNeedsUpdate;
|
|
};
|
|
|
|
/**
|
|
* Update the content of a module on screen.
|
|
* @param {Module} module The module to check.
|
|
* @param {string} newHeader The new header that is generated.
|
|
* @param {HTMLElement} newContent The new content that is generated.
|
|
*/
|
|
const updateModuleContent = function (module, newHeader, newContent) {
|
|
const moduleWrapper = document.getElementById(module.identifier);
|
|
if (moduleWrapper === null) {
|
|
return;
|
|
}
|
|
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
|
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
|
|
|
contentWrapper[0].innerHTML = "";
|
|
contentWrapper[0].appendChild(newContent);
|
|
|
|
headerWrapper[0].innerHTML = newHeader;
|
|
if (headerWrapper.length > 0 && newHeader) {
|
|
headerWrapper[0].style.display = "block";
|
|
} else {
|
|
headerWrapper[0].style.display = "none";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Hide the module.
|
|
* @param {Module} module The module to hide.
|
|
* @param {number} speed The speed of the hide animation.
|
|
* @param {Function} callback Called when the animation is done.
|
|
* @param {object} [options] Optional settings for the hide method.
|
|
*/
|
|
const hideModule = function (module, speed, callback, options = {}) {
|
|
// set lockString if set in options.
|
|
if (options.lockString) {
|
|
// Log.log("Has lockstring: " + options.lockString);
|
|
if (module.lockStrings.indexOf(options.lockString) === -1) {
|
|
module.lockStrings.push(options.lockString);
|
|
}
|
|
}
|
|
|
|
const moduleWrapper = document.getElementById(module.identifier);
|
|
if (moduleWrapper !== null) {
|
|
clearTimeout(module.showHideTimer);
|
|
// reset all animations if needed
|
|
if (module.hasAnimateOut) {
|
|
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
|
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
|
|
module.hasAnimateOut = false;
|
|
}
|
|
if (module.hasAnimateIn) {
|
|
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
|
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
|
module.hasAnimateIn = false;
|
|
}
|
|
// haveAnimateName for verify if we are using AninateCSS library
|
|
// we check AnimateCSSOut Array for validate it
|
|
// and finaly return the animate name or `null` (for default MM² animation)
|
|
let haveAnimateName = null;
|
|
// check if have valid animateOut in module definition (module.data.animateOut)
|
|
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
|
|
// can't be override with options.animate
|
|
else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
|
|
|
if (haveAnimateName) {
|
|
// with AnimateCSS
|
|
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
|
|
module.hasAnimateOut = haveAnimateName;
|
|
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
|
module.showHideTimer = setTimeout(function () {
|
|
removeAnimateCSS(module.identifier, haveAnimateName);
|
|
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
|
|
// AnimateCSS is now done
|
|
moduleWrapper.style.opacity = 0;
|
|
moduleWrapper.classList.add("hidden");
|
|
moduleWrapper.style.position = "fixed";
|
|
module.hasAnimateOut = false;
|
|
|
|
updateWrapperStates();
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}, speed);
|
|
} else {
|
|
// default MM² Animate
|
|
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
|
moduleWrapper.style.opacity = 0;
|
|
moduleWrapper.classList.add("hidden");
|
|
module.showHideTimer = setTimeout(function () {
|
|
// To not take up any space, we just make the position absolute.
|
|
// since it's fade out anyway, we can see it lay above or
|
|
// below other modules. This works way better than adjusting
|
|
// the .display property.
|
|
moduleWrapper.style.position = "fixed";
|
|
|
|
updateWrapperStates();
|
|
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}, speed);
|
|
}
|
|
} else {
|
|
// invoke callback even if no content, issue 1308
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Show the module.
|
|
* @param {Module} module The module to show.
|
|
* @param {number} speed The speed of the show animation.
|
|
* @param {Function} callback Called when the animation is done.
|
|
* @param {object} [options] Optional settings for the show method.
|
|
*/
|
|
const showModule = function (module, speed, callback, options = {}) {
|
|
// remove lockString if set in options.
|
|
if (options.lockString) {
|
|
const index = module.lockStrings.indexOf(options.lockString);
|
|
if (index !== -1) {
|
|
module.lockStrings.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
// Check if there are no more lockstrings set, or the force option is set.
|
|
// Otherwise cancel show action.
|
|
if (module.lockStrings.length !== 0 && options.force !== true) {
|
|
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
|
if (typeof options.onError === "function") {
|
|
options.onError(new Error("LOCK_STRING_ACTIVE"));
|
|
}
|
|
return;
|
|
}
|
|
// reset all animations if needed
|
|
if (module.hasAnimateOut) {
|
|
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
|
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
|
|
module.hasAnimateOut = false;
|
|
}
|
|
if (module.hasAnimateIn) {
|
|
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
|
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
|
|
module.hasAnimateIn = false;
|
|
}
|
|
|
|
module.hidden = false;
|
|
|
|
// If forced show, clean current lockstrings.
|
|
if (module.lockStrings.length !== 0 && options.force === true) {
|
|
Log.log(`Force show of module: ${module.name}`);
|
|
module.lockStrings = [];
|
|
}
|
|
|
|
const moduleWrapper = document.getElementById(module.identifier);
|
|
if (moduleWrapper !== null) {
|
|
clearTimeout(module.showHideTimer);
|
|
|
|
// haveAnimateName for verify if we are using AninateCSS library
|
|
// we check AnimateCSSIn Array for validate it
|
|
// and finaly return the animate name or `null` (for default MM² animation)
|
|
let haveAnimateName = null;
|
|
// check if have valid animateOut in module definition (module.data.animateIn)
|
|
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
|
|
// can't be override with options.animate
|
|
else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
|
|
|
if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
|
// Restore the position. See hideModule() for more info.
|
|
moduleWrapper.style.position = "static";
|
|
moduleWrapper.classList.remove("hidden");
|
|
|
|
updateWrapperStates();
|
|
|
|
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
|
|
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
|
moduleWrapper.style.opacity = 1;
|
|
|
|
if (haveAnimateName) {
|
|
// with AnimateCSS
|
|
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
|
|
module.hasAnimateIn = haveAnimateName;
|
|
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
|
module.showHideTimer = setTimeout(function () {
|
|
removeAnimateCSS(module.identifier, haveAnimateName);
|
|
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
|
|
module.hasAnimateIn = false;
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}, speed);
|
|
} else {
|
|
// default MM² Animate
|
|
module.showHideTimer = setTimeout(function () {
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}, speed);
|
|
}
|
|
} else {
|
|
// invoke callback
|
|
if (typeof callback === "function") {
|
|
callback();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks for all positions if it has visible content.
|
|
* If not, if will hide the position to prevent unwanted margins.
|
|
* This method should be called by the show and hide methods.
|
|
*
|
|
* Example:
|
|
* If the top_bar only contains the update notification. And no update is available,
|
|
* the update notification is hidden. The top bar still occupies space making for
|
|
* an ugly top margin. By using this function, the top bar will be hidden if the
|
|
* update notification is not visible.
|
|
*/
|
|
const updateWrapperStates = function () {
|
|
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
|
|
|
positions.forEach(function (position) {
|
|
const wrapper = selectWrapper(position);
|
|
const moduleWrappers = wrapper.getElementsByClassName("module");
|
|
|
|
let showWrapper = false;
|
|
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
|
|
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
|
showWrapper = true;
|
|
}
|
|
});
|
|
|
|
wrapper.style.display = showWrapper ? "block" : "none";
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Loads the core config and combines it with the system defaults.
|
|
*/
|
|
const loadConfig = function () {
|
|
// FIXME: Think about how to pass config around without breaking tests
|
|
if (typeof config === "undefined") {
|
|
config = defaults;
|
|
Log.error("Config file is missing! Please create a config file.");
|
|
return;
|
|
}
|
|
|
|
config = Object.assign({}, defaults, config);
|
|
};
|
|
|
|
/**
|
|
* Adds special selectors on a collection of modules.
|
|
* @param {Module[]} modules Array of modules.
|
|
*/
|
|
const setSelectionMethodsForModules = function (modules) {
|
|
|
|
/**
|
|
* Filter modules with the specified classes.
|
|
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
|
* @returns {Module[]} Filtered collection of modules.
|
|
*/
|
|
const withClass = function (className) {
|
|
return modulesByClass(className, true);
|
|
};
|
|
|
|
/**
|
|
* Filter modules without the specified classes.
|
|
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
|
* @returns {Module[]} Filtered collection of modules.
|
|
*/
|
|
const exceptWithClass = function (className) {
|
|
return modulesByClass(className, false);
|
|
};
|
|
|
|
/**
|
|
* Filters a collection of modules based on classname(s).
|
|
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
|
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
|
* @returns {Module[]} Filtered collection of modules.
|
|
*/
|
|
const modulesByClass = function (className, include) {
|
|
let searchClasses = className;
|
|
if (typeof className === "string") {
|
|
searchClasses = className.split(" ");
|
|
}
|
|
|
|
const newModules = modules.filter(function (module) {
|
|
const classes = module.data.classes.toLowerCase().split(" ");
|
|
|
|
for (const searchClass of searchClasses) {
|
|
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
|
|
return include;
|
|
}
|
|
}
|
|
|
|
return !include;
|
|
});
|
|
|
|
setSelectionMethodsForModules(newModules);
|
|
return newModules;
|
|
};
|
|
|
|
/**
|
|
* Removes a module instance from the collection.
|
|
* @param {object} module The module instance to remove from the collection.
|
|
* @returns {Module[]} Filtered collection of modules.
|
|
*/
|
|
const exceptModule = function (module) {
|
|
const newModules = modules.filter(function (mod) {
|
|
return mod.identifier !== module.identifier;
|
|
});
|
|
|
|
setSelectionMethodsForModules(newModules);
|
|
return newModules;
|
|
};
|
|
|
|
/**
|
|
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
|
* @param {Function} callback The function to execute with the module as an argument.
|
|
*/
|
|
const enumerate = function (callback) {
|
|
modules.map(function (module) {
|
|
callback(module);
|
|
});
|
|
};
|
|
|
|
if (typeof modules.withClass === "undefined") {
|
|
Object.defineProperty(modules, "withClass", { value: withClass, enumerable: false });
|
|
}
|
|
if (typeof modules.exceptWithClass === "undefined") {
|
|
Object.defineProperty(modules, "exceptWithClass", { value: exceptWithClass, enumerable: false });
|
|
}
|
|
if (typeof modules.exceptModule === "undefined") {
|
|
Object.defineProperty(modules, "exceptModule", { value: exceptModule, enumerable: false });
|
|
}
|
|
if (typeof modules.enumerate === "undefined") {
|
|
Object.defineProperty(modules, "enumerate", { value: enumerate, enumerable: false });
|
|
}
|
|
};
|
|
|
|
return {
|
|
|
|
/* Public Methods */
|
|
|
|
/**
|
|
* Main init method.
|
|
*/
|
|
async init () {
|
|
Log.info("Initializing MagicMirror².");
|
|
loadConfig();
|
|
|
|
Log.setLogLevel(config.logLevel);
|
|
|
|
await Translator.loadCoreTranslations(config.language);
|
|
await Loader.loadModules();
|
|
},
|
|
|
|
/**
|
|
* Gets called when all modules are started.
|
|
* @param {Module[]} moduleObjects All module instances.
|
|
*/
|
|
modulesStarted (moduleObjects) {
|
|
modules = [];
|
|
let startUp = "";
|
|
|
|
moduleObjects.forEach((module) => modules.push(module));
|
|
|
|
Log.info("All modules started!");
|
|
sendNotification("ALL_MODULES_STARTED");
|
|
|
|
createDomObjects();
|
|
|
|
if (config.reloadAfterServerRestart) {
|
|
setInterval(async () => {
|
|
// if server startup time has changed (which means server was restarted)
|
|
// the client reloads the mm page
|
|
try {
|
|
const res = await fetch(`${location.protocol}//${location.host}/startup`);
|
|
const curr = await res.text();
|
|
if (startUp === "") startUp = curr;
|
|
if (startUp !== curr) {
|
|
startUp = "";
|
|
window.location.reload(true);
|
|
console.warn("Refreshing Website because server was restarted");
|
|
}
|
|
} catch (err) {
|
|
Log.error(`MagicMirror not reachable: ${err}`);
|
|
}
|
|
}, config.checkServerInterval);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Send a notification to all modules.
|
|
* @param {string} notification The identifier of the notification.
|
|
* @param {*} payload The payload of the notification.
|
|
* @param {Module} sender The module that sent the notification.
|
|
*/
|
|
sendNotification (notification, payload, sender) {
|
|
if (arguments.length < 3) {
|
|
Log.error("sendNotification: Missing arguments.");
|
|
return;
|
|
}
|
|
|
|
if (typeof notification !== "string") {
|
|
Log.error("sendNotification: Notification should be a string.");
|
|
return;
|
|
}
|
|
|
|
if (!(sender instanceof Module)) {
|
|
Log.error("sendNotification: Sender should be a module.");
|
|
return;
|
|
}
|
|
|
|
// Further implementation is done in the private method.
|
|
sendNotification(notification, payload, sender);
|
|
},
|
|
|
|
/**
|
|
* Update the dom for a specific module.
|
|
* @param {Module} module The module that needs an update.
|
|
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
|
*/
|
|
updateDom (module, updateOptions) {
|
|
if (!(module instanceof Module)) {
|
|
Log.error("updateDom: Sender should be a module.");
|
|
return;
|
|
}
|
|
|
|
if (!module.data.position) {
|
|
Log.warn("module tries to update the DOM without being displayed.");
|
|
return;
|
|
}
|
|
|
|
// Further implementation is done in the private method.
|
|
updateDom(module, updateOptions);
|
|
},
|
|
|
|
/**
|
|
* Returns a collection of all modules currently active.
|
|
* @returns {Module[]} A collection of all modules currently active.
|
|
*/
|
|
getModules () {
|
|
setSelectionMethodsForModules(modules);
|
|
return modules;
|
|
},
|
|
|
|
/**
|
|
* Hide the module.
|
|
* @param {Module} module The module to hide.
|
|
* @param {number} speed The speed of the hide animation.
|
|
* @param {Function} callback Called when the animation is done.
|
|
* @param {object} [options] Optional settings for the hide method.
|
|
*/
|
|
hideModule (module, speed, callback, options) {
|
|
module.hidden = true;
|
|
hideModule(module, speed, callback, options);
|
|
},
|
|
|
|
/**
|
|
* Show the module.
|
|
* @param {Module} module The module to show.
|
|
* @param {number} speed The speed of the show animation.
|
|
* @param {Function} callback Called when the animation is done.
|
|
* @param {object} [options] Optional settings for the show method.
|
|
*/
|
|
showModule (module, speed, callback, options) {
|
|
// do not change module.hidden yet, only if we really show it later
|
|
showModule(module, speed, callback, options);
|
|
}
|
|
};
|
|
}());
|
|
|
|
// Add polyfill for Object.assign.
|
|
if (typeof Object.assign !== "function") {
|
|
(function () {
|
|
Object.assign = function (target) {
|
|
"use strict";
|
|
if (target === undefined || target === null) {
|
|
throw new TypeError("Cannot convert undefined or null to object");
|
|
}
|
|
const output = Object(target);
|
|
for (let index = 1; index < arguments.length; index++) {
|
|
const source = arguments[index];
|
|
if (source !== undefined && source !== null) {
|
|
for (const nextKey in source) {
|
|
if (source.hasOwnProperty(nextKey)) {
|
|
output[nextKey] = source[nextKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
};
|
|
}());
|
|
}
|
|
|
|
MM.init();
|