Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. /* global Loader, defaults, Translator */
  2. /* Magic Mirror
  3. * Main System
  4. *
  5. * By Michael Teeuw https://michaelteeuw.nl
  6. * MIT Licensed.
  7. */
  8. const MM = (function () {
  9. let modules = [];
  10. /* Private Methods */
  11. /**
  12. * Create dom objects for all modules that are configured for a specific position.
  13. */
  14. const createDomObjects = function () {
  15. const domCreationPromises = [];
  16. modules.forEach(function (module) {
  17. if (typeof module.data.position !== "string") {
  18. return;
  19. }
  20. const wrapper = selectWrapper(module.data.position);
  21. const dom = document.createElement("div");
  22. dom.id = module.identifier;
  23. dom.className = module.name;
  24. if (typeof module.data.classes === "string") {
  25. dom.className = "module " + dom.className + " " + module.data.classes;
  26. }
  27. dom.opacity = 0;
  28. wrapper.appendChild(dom);
  29. const moduleHeader = document.createElement("header");
  30. moduleHeader.innerHTML = module.getHeader();
  31. moduleHeader.className = "module-header";
  32. dom.appendChild(moduleHeader);
  33. if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
  34. moduleHeader.style.display = "none;";
  35. } else {
  36. moduleHeader.style.display = "block;";
  37. }
  38. const moduleContent = document.createElement("div");
  39. moduleContent.className = "module-content";
  40. dom.appendChild(moduleContent);
  41. const domCreationPromise = updateDom(module, 0);
  42. domCreationPromises.push(domCreationPromise);
  43. domCreationPromise
  44. .then(function () {
  45. sendNotification("MODULE_DOM_CREATED", null, null, module);
  46. })
  47. .catch(Log.error);
  48. });
  49. updateWrapperStates();
  50. Promise.all(domCreationPromises).then(function () {
  51. sendNotification("DOM_OBJECTS_CREATED");
  52. });
  53. };
  54. /**
  55. * Select the wrapper dom object for a specific position.
  56. *
  57. * @param {string} position The name of the position.
  58. * @returns {HTMLElement} the wrapper element
  59. */
  60. const selectWrapper = function (position) {
  61. const classes = position.replace("_", " ");
  62. const parentWrapper = document.getElementsByClassName(classes);
  63. if (parentWrapper.length > 0) {
  64. const wrapper = parentWrapper[0].getElementsByClassName("container");
  65. if (wrapper.length > 0) {
  66. return wrapper[0];
  67. }
  68. }
  69. };
  70. /**
  71. * Send a notification to all modules.
  72. *
  73. * @param {string} notification The identifier of the notification.
  74. * @param {*} payload The payload of the notification.
  75. * @param {Module} sender The module that sent the notification.
  76. * @param {Module} [sendTo] The (optional) module to send the notification to.
  77. */
  78. const sendNotification = function (notification, payload, sender, sendTo) {
  79. for (const m in modules) {
  80. const module = modules[m];
  81. if (module !== sender && (!sendTo || module === sendTo)) {
  82. module.notificationReceived(notification, payload, sender);
  83. }
  84. }
  85. };
  86. /**
  87. * Update the dom for a specific module.
  88. *
  89. * @param {Module} module The module that needs an update.
  90. * @param {number} [speed] The (optional) number of microseconds for the animation.
  91. * @returns {Promise} Resolved when the dom is fully updated.
  92. */
  93. const updateDom = function (module, speed) {
  94. return new Promise(function (resolve) {
  95. const newHeader = module.getHeader();
  96. let newContentPromise = module.getDom();
  97. if (!(newContentPromise instanceof Promise)) {
  98. // convert to a promise if not already one to avoid if/else's everywhere
  99. newContentPromise = Promise.resolve(newContentPromise);
  100. }
  101. newContentPromise
  102. .then(function (newContent) {
  103. const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
  104. updatePromise.then(resolve).catch(Log.error);
  105. })
  106. .catch(Log.error);
  107. });
  108. };
  109. /**
  110. * Update the dom with the specified content
  111. *
  112. * @param {Module} module The module that needs an update.
  113. * @param {number} [speed] The (optional) number of microseconds for the animation.
  114. * @param {string} newHeader The new header that is generated.
  115. * @param {HTMLElement} newContent The new content that is generated.
  116. * @returns {Promise} Resolved when the module dom has been updated.
  117. */
  118. const updateDomWithContent = function (module, speed, newHeader, newContent) {
  119. return new Promise(function (resolve) {
  120. if (module.hidden || !speed) {
  121. updateModuleContent(module, newHeader, newContent);
  122. resolve();
  123. return;
  124. }
  125. if (!moduleNeedsUpdate(module, newHeader, newContent)) {
  126. resolve();
  127. return;
  128. }
  129. if (!speed) {
  130. updateModuleContent(module, newHeader, newContent);
  131. resolve();
  132. return;
  133. }
  134. hideModule(module, speed / 2, function () {
  135. updateModuleContent(module, newHeader, newContent);
  136. if (!module.hidden) {
  137. showModule(module, speed / 2);
  138. }
  139. resolve();
  140. });
  141. });
  142. };
  143. /**
  144. * Check if the content has changed.
  145. *
  146. * @param {Module} module The module to check.
  147. * @param {string} newHeader The new header that is generated.
  148. * @param {HTMLElement} newContent The new content that is generated.
  149. * @returns {boolean} True if the module need an update, false otherwise
  150. */
  151. const moduleNeedsUpdate = function (module, newHeader, newContent) {
  152. const moduleWrapper = document.getElementById(module.identifier);
  153. if (moduleWrapper === null) {
  154. return false;
  155. }
  156. const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
  157. const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
  158. let headerNeedsUpdate = false;
  159. let contentNeedsUpdate;
  160. if (headerWrapper.length > 0) {
  161. headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
  162. }
  163. const tempContentWrapper = document.createElement("div");
  164. tempContentWrapper.appendChild(newContent);
  165. contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
  166. return headerNeedsUpdate || contentNeedsUpdate;
  167. };
  168. /**
  169. * Update the content of a module on screen.
  170. *
  171. * @param {Module} module The module to check.
  172. * @param {string} newHeader The new header that is generated.
  173. * @param {HTMLElement} newContent The new content that is generated.
  174. */
  175. const updateModuleContent = function (module, newHeader, newContent) {
  176. const moduleWrapper = document.getElementById(module.identifier);
  177. if (moduleWrapper === null) {
  178. return;
  179. }
  180. const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
  181. const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
  182. contentWrapper[0].innerHTML = "";
  183. contentWrapper[0].appendChild(newContent);
  184. headerWrapper[0].innerHTML = newHeader;
  185. if (headerWrapper.length > 0 && newHeader) {
  186. headerWrapper[0].style.display = "block";
  187. } else {
  188. headerWrapper[0].style.display = "none";
  189. }
  190. };
  191. /**
  192. * Hide the module.
  193. *
  194. * @param {Module} module The module to hide.
  195. * @param {number} speed The speed of the hide animation.
  196. * @param {Function} callback Called when the animation is done.
  197. * @param {object} [options] Optional settings for the hide method.
  198. */
  199. const hideModule = function (module, speed, callback, options) {
  200. options = options || {};
  201. // set lockString if set in options.
  202. if (options.lockString) {
  203. // Log.log("Has lockstring: " + options.lockString);
  204. if (module.lockStrings.indexOf(options.lockString) === -1) {
  205. module.lockStrings.push(options.lockString);
  206. }
  207. }
  208. const moduleWrapper = document.getElementById(module.identifier);
  209. if (moduleWrapper !== null) {
  210. moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
  211. moduleWrapper.style.opacity = 0;
  212. clearTimeout(module.showHideTimer);
  213. module.showHideTimer = setTimeout(function () {
  214. // To not take up any space, we just make the position absolute.
  215. // since it's fade out anyway, we can see it lay above or
  216. // below other modules. This works way better than adjusting
  217. // the .display property.
  218. moduleWrapper.style.position = "fixed";
  219. updateWrapperStates();
  220. if (typeof callback === "function") {
  221. callback();
  222. }
  223. }, speed);
  224. } else {
  225. // invoke callback even if no content, issue 1308
  226. if (typeof callback === "function") {
  227. callback();
  228. }
  229. }
  230. };
  231. /**
  232. * Show the module.
  233. *
  234. * @param {Module} module The module to show.
  235. * @param {number} speed The speed of the show animation.
  236. * @param {Function} callback Called when the animation is done.
  237. * @param {object} [options] Optional settings for the show method.
  238. */
  239. const showModule = function (module, speed, callback, options) {
  240. options = options || {};
  241. // remove lockString if set in options.
  242. if (options.lockString) {
  243. const index = module.lockStrings.indexOf(options.lockString);
  244. if (index !== -1) {
  245. module.lockStrings.splice(index, 1);
  246. }
  247. }
  248. // Check if there are no more lockstrings set, or the force option is set.
  249. // Otherwise cancel show action.
  250. if (module.lockStrings.length !== 0 && options.force !== true) {
  251. Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
  252. if (typeof options.onError === "function") {
  253. options.onError(new Error("LOCK_STRING_ACTIVE"));
  254. }
  255. return;
  256. }
  257. module.hidden = false;
  258. // If forced show, clean current lockstrings.
  259. if (module.lockStrings.length !== 0 && options.force === true) {
  260. Log.log("Force show of module: " + module.name);
  261. module.lockStrings = [];
  262. }
  263. const moduleWrapper = document.getElementById(module.identifier);
  264. if (moduleWrapper !== null) {
  265. moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
  266. // Restore the position. See hideModule() for more info.
  267. moduleWrapper.style.position = "static";
  268. updateWrapperStates();
  269. // Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
  270. const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
  271. moduleWrapper.style.opacity = 1;
  272. clearTimeout(module.showHideTimer);
  273. module.showHideTimer = setTimeout(function () {
  274. if (typeof callback === "function") {
  275. callback();
  276. }
  277. }, speed);
  278. } else {
  279. // invoke callback
  280. if (typeof callback === "function") {
  281. callback();
  282. }
  283. }
  284. };
  285. /**
  286. * Checks for all positions if it has visible content.
  287. * If not, if will hide the position to prevent unwanted margins.
  288. * This method should be called by the show and hide methods.
  289. *
  290. * Example:
  291. * If the top_bar only contains the update notification. And no update is available,
  292. * the update notification is hidden. The top bar still occupies space making for
  293. * an ugly top margin. By using this function, the top bar will be hidden if the
  294. * update notification is not visible.
  295. */
  296. const updateWrapperStates = function () {
  297. 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"];
  298. positions.forEach(function (position) {
  299. const wrapper = selectWrapper(position);
  300. const moduleWrappers = wrapper.getElementsByClassName("module");
  301. let showWrapper = false;
  302. Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
  303. if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
  304. showWrapper = true;
  305. }
  306. });
  307. wrapper.style.display = showWrapper ? "block" : "none";
  308. });
  309. };
  310. /**
  311. * Loads the core config and combines it with the system defaults.
  312. */
  313. const loadConfig = function () {
  314. // FIXME: Think about how to pass config around without breaking tests
  315. /* eslint-disable */
  316. if (typeof config === "undefined") {
  317. config = defaults;
  318. Log.error("Config file is missing! Please create a config file.");
  319. return;
  320. }
  321. config = Object.assign({}, defaults, config);
  322. /* eslint-enable */
  323. };
  324. /**
  325. * Adds special selectors on a collection of modules.
  326. *
  327. * @param {Module[]} modules Array of modules.
  328. */
  329. const setSelectionMethodsForModules = function (modules) {
  330. /**
  331. * Filter modules with the specified classes.
  332. *
  333. * @param {string|string[]} className one or multiple classnames (array or space divided).
  334. * @returns {Module[]} Filtered collection of modules.
  335. */
  336. const withClass = function (className) {
  337. return modulesByClass(className, true);
  338. };
  339. /**
  340. * Filter modules without the specified classes.
  341. *
  342. * @param {string|string[]} className one or multiple classnames (array or space divided).
  343. * @returns {Module[]} Filtered collection of modules.
  344. */
  345. const exceptWithClass = function (className) {
  346. return modulesByClass(className, false);
  347. };
  348. /**
  349. * Filters a collection of modules based on classname(s).
  350. *
  351. * @param {string|string[]} className one or multiple classnames (array or space divided).
  352. * @param {boolean} include if the filter should include or exclude the modules with the specific classes.
  353. * @returns {Module[]} Filtered collection of modules.
  354. */
  355. const modulesByClass = function (className, include) {
  356. let searchClasses = className;
  357. if (typeof className === "string") {
  358. searchClasses = className.split(" ");
  359. }
  360. const newModules = modules.filter(function (module) {
  361. const classes = module.data.classes.toLowerCase().split(" ");
  362. for (const searchClass of searchClasses) {
  363. if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
  364. return include;
  365. }
  366. }
  367. return !include;
  368. });
  369. setSelectionMethodsForModules(newModules);
  370. return newModules;
  371. };
  372. /**
  373. * Removes a module instance from the collection.
  374. *
  375. * @param {object} module The module instance to remove from the collection.
  376. * @returns {Module[]} Filtered collection of modules.
  377. */
  378. const exceptModule = function (module) {
  379. const newModules = modules.filter(function (mod) {
  380. return mod.identifier !== module.identifier;
  381. });
  382. setSelectionMethodsForModules(newModules);
  383. return newModules;
  384. };
  385. /**
  386. * Walks thru a collection of modules and executes the callback with the module as an argument.
  387. *
  388. * @param {Function} callback The function to execute with the module as an argument.
  389. */
  390. const enumerate = function (callback) {
  391. modules.map(function (module) {
  392. callback(module);
  393. });
  394. };
  395. if (typeof modules.withClass === "undefined") {
  396. Object.defineProperty(modules, "withClass", { value: withClass, enumerable: false });
  397. }
  398. if (typeof modules.exceptWithClass === "undefined") {
  399. Object.defineProperty(modules, "exceptWithClass", { value: exceptWithClass, enumerable: false });
  400. }
  401. if (typeof modules.exceptModule === "undefined") {
  402. Object.defineProperty(modules, "exceptModule", { value: exceptModule, enumerable: false });
  403. }
  404. if (typeof modules.enumerate === "undefined") {
  405. Object.defineProperty(modules, "enumerate", { value: enumerate, enumerable: false });
  406. }
  407. };
  408. return {
  409. /* Public Methods */
  410. /**
  411. * Main init method.
  412. */
  413. init: function () {
  414. Log.info("Initializing MagicMirror.");
  415. loadConfig();
  416. Log.setLogLevel(config.logLevel);
  417. Translator.loadCoreTranslations(config.language);
  418. Loader.loadModules();
  419. },
  420. /**
  421. * Gets called when all modules are started.
  422. *
  423. * @param {Module[]} moduleObjects All module instances.
  424. */
  425. modulesStarted: function (moduleObjects) {
  426. modules = [];
  427. moduleObjects.forEach((module) => modules.push(module));
  428. Log.info("All modules started!");
  429. sendNotification("ALL_MODULES_STARTED");
  430. createDomObjects();
  431. },
  432. /**
  433. * Send a notification to all modules.
  434. *
  435. * @param {string} notification The identifier of the notification.
  436. * @param {*} payload The payload of the notification.
  437. * @param {Module} sender The module that sent the notification.
  438. */
  439. sendNotification: function (notification, payload, sender) {
  440. if (arguments.length < 3) {
  441. Log.error("sendNotification: Missing arguments.");
  442. return;
  443. }
  444. if (typeof notification !== "string") {
  445. Log.error("sendNotification: Notification should be a string.");
  446. return;
  447. }
  448. if (!(sender instanceof Module)) {
  449. Log.error("sendNotification: Sender should be a module.");
  450. return;
  451. }
  452. // Further implementation is done in the private method.
  453. sendNotification(notification, payload, sender);
  454. },
  455. /**
  456. * Update the dom for a specific module.
  457. *
  458. * @param {Module} module The module that needs an update.
  459. * @param {number} [speed] The number of microseconds for the animation.
  460. */
  461. updateDom: function (module, speed) {
  462. if (!(module instanceof Module)) {
  463. Log.error("updateDom: Sender should be a module.");
  464. return;
  465. }
  466. if (!module.data.position) {
  467. Log.warn("module tries to update the DOM without being displayed.");
  468. return;
  469. }
  470. // Further implementation is done in the private method.
  471. updateDom(module, speed);
  472. },
  473. /**
  474. * Returns a collection of all modules currently active.
  475. *
  476. * @returns {Module[]} A collection of all modules currently active.
  477. */
  478. getModules: function () {
  479. setSelectionMethodsForModules(modules);
  480. return modules;
  481. },
  482. /**
  483. * Hide the module.
  484. *
  485. * @param {Module} module The module to hide.
  486. * @param {number} speed The speed of the hide animation.
  487. * @param {Function} callback Called when the animation is done.
  488. * @param {object} [options] Optional settings for the hide method.
  489. */
  490. hideModule: function (module, speed, callback, options) {
  491. module.hidden = true;
  492. hideModule(module, speed, callback, options);
  493. },
  494. /**
  495. * Show the module.
  496. *
  497. * @param {Module} module The module to show.
  498. * @param {number} speed The speed of the show animation.
  499. * @param {Function} callback Called when the animation is done.
  500. * @param {object} [options] Optional settings for the show method.
  501. */
  502. showModule: function (module, speed, callback, options) {
  503. // do not change module.hidden yet, only if we really show it later
  504. showModule(module, speed, callback, options);
  505. }
  506. };
  507. })();
  508. // Add polyfill for Object.assign.
  509. if (typeof Object.assign !== "function") {
  510. (function () {
  511. Object.assign = function (target) {
  512. "use strict";
  513. if (target === undefined || target === null) {
  514. throw new TypeError("Cannot convert undefined or null to object");
  515. }
  516. const output = Object(target);
  517. for (let index = 1; index < arguments.length; index++) {
  518. const source = arguments[index];
  519. if (source !== undefined && source !== null) {
  520. for (const nextKey in source) {
  521. if (source.hasOwnProperty(nextKey)) {
  522. output[nextKey] = source[nextKey];
  523. }
  524. }
  525. }
  526. }
  527. return output;
  528. };
  529. })();
  530. }
  531. MM.init();