mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-04-24 22:47:08 -04:00
## [2.30.0] - 2025-01-01 Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel, @KristjanESPERANTO, @rejas, @sdetweil. > ⚠️ This release needs nodejs version `v20` or `v22 or higher`, minimum version is `v20.18.1` ### Added - [core] Add wayland and windows start options to `package.json` (#3594) - [docs] Add step for npm publishing in release process (#3595) - [core] Add GitHub workflow to run spellcheck a few days before each release (#3623) - [core] Add test flag to `index.html` to pass to module js for test mode detection (needed by #3630) - [core] Add export on animation names (#3644) - [compliments] Add support for refreshing remote compliments file, and test cases (#3630) - [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9 (#3586) - [linter] Re-activate `eslint-plugin-package-json` to lint `package.json` (#3643) - [linter] Add linting for markdown files (#3646) - [linter] Add some handy ESLint rules. - [calendar] Add ability to display end date for full date events, where end is not same day (showEnd=true) (#3650) - [core] Add text to the config.js.sample file about the locale variable (#3654, #3655) - [core] Add fetch timeout for all node_helpers (thru undici, forces node 20.18.1 minimum) to help on slower systems. (#3660) (3661) ### Changed - [core] Run code style checks in workflow only once (#3648) - [core] Fix animations export #3644 only on server side (#3649) - [core] Use project URL in fallback config (#3656) - [core] Fix Access Denied crash writing js/positions.js (on synology nas) #3651. new message, MM starts, but no modules showing (#3652) - [linter] Switch to 'npx' for lint-staged in pre-commit hook (#3658) ### Removed - [tests] Remove `node-pty` and `drivelist` from rebuilded test (#3575) - [deps] Remove `@eslint/js` dependency. Already installed with `eslint` in deep (#3636) ### Updated - [repo] Reactivate `stale.yaml` as GitHub action to mark issues as stale after 60 days and close them 7 days later (if no activity) (#3577, #3580, #3581) - [core] Update electron dependency to v32 (test electron rebuild) and all other dependencies too (#3657) - [tests] All test configs have been updated to allow full external access, allowing for easier debugging (especially when running as a container) - [core] Run and test with node 23 (#3588) - [workflow] delete exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (#3659) ### Fixed - [updatenotification] Fix pm2 using detection when pm2 script is inside or outside MagicMirror root folder (#3576) (#3605) (#3626) (#3628) - [core] Fix loading node_helper of modules: avoid black screen, display errors and continue loading with next module (#3578) - [weather] Change default value for weatherEndpoint of provider openweathermap to "/onecall" (#3574) - [tests] Fix electron tests with mock dates, the mock on server side was missing (#3597) - [tests] Fix testcases with hard coded Date.now (#3597) - [core] Fix missing `basePath` where `location.host` is used (#3613) - [compliments] croner library changed filenames used in latest version (#3624) - [linter] Fix ESLint ignore pattern which caused that default modules not to be linted (#3632) - [core] Fix module path in case of sub/sub folder is used and use path.resolve for resolve `moduleFolder` and `defaultModuleFolder` in app.js (#3653) - [calendar] Update to resolve issues #3098 #3144 #3351 #3422 #3443 #3467 #3537 related to timezone changes - [calendar] Fix #3267 (styles array), also fixes event with both exdate AND recurrence(and testcase) - [calendar] Fix showEndsOnlyWithDuration not working, #3598, applies ONLY to full day events - [calendar] Fix showEnd for Full Day events (#3602) - [tests] Suppress "module is not defined" in e2e tests (#3647) - [calendar] Fix #3267 (styles array, really this time!) - [core] Fix #3662 js/positions.js created incorrectly --------- 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: Veeck <github@veeck.de> 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: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> 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>
737 lines
24 KiB
JavaScript
737 lines
24 KiB
JavaScript
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
|
|
|
|
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 AnimateCSS library
|
|
// we check AnimateCSSOut Array for validate it
|
|
// and finally 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 AnimateCSS library
|
|
// we check AnimateCSSIn Array for validate it
|
|
// and finally 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 () {
|
|
modulePositions.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}${config.basePath}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).then(function () {
|
|
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
|
|
sendNotification("MODULE_DOM_UPDATED", null, null, module);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
// Return all available module positions.
|
|
getAvailableModulePositions: modulePositions
|
|
};
|
|
}());
|
|
|
|
// 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();
|