diff --git a/.jscsrc b/.jscsrc index bc1fcea6..4638b44f 100644 --- a/.jscsrc +++ b/.jscsrc @@ -4,5 +4,6 @@ "validateQuoteMarks": "\"", "maximumLineLength": 250, "requireCurlyBraces": [], - "requireCamelCaseOrUpperCaseIdentifiers": false + "requireCamelCaseOrUpperCaseIdentifiers": false, + "excludeFiles": [".jscsrc"], } \ No newline at end of file diff --git a/README.md b/README.md index 4c935d31..12dada9a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec - [Configuration](#configuration) - [Modules](#modules) - [Known Issues](#known-issues) +- [community](#community) - [Contributing Guidelines](#contributing-guidelines) ## Usage @@ -110,6 +111,10 @@ For more available modules, check out out the wiki page: [MagicMirror² Modules] - Electron seems to have some issues on certain Raspberry Pi 2's. See [#145](https://github.com/MichMich/MagicMirror/issues/145). - MagicMirror² (Electron) sometimes quits without an error after an extended period of use. See [#150](https://github.com/MichMich/MagicMirror/issues/150). +## Community + +The community around the MagicMirror² is constantly growing. We even have a [forum](https://forum.magicmirror.builders) now where you can share your ideas, ask questions, help others and get inspired by other builders. We would love to see you there! + ## Contributing Guidelines Contributions of all kinds are welcome, not only in the form of code but also with regards bug reports and documentation. diff --git a/index.html b/index.html index d08291cc..26972810 100644 --- a/index.html +++ b/index.html @@ -6,8 +6,8 @@ - +
diff --git a/installers/raspberry.sh b/installers/raspberry.sh index 4f1d2e2c..3c1687c0 100644 --- a/installers/raspberry.sh +++ b/installers/raspberry.sh @@ -18,7 +18,7 @@ echo "Installing helper tools ..." sudo apt-get install curl wget build-essential unzip || exit ARM=$(uname -m) # Determine which Pi is running. -NODE_LATEST="v5.11.0" # Set the latest version here. +NODE_LATEST="v6.0.0" # Set the latest version here. DOWNLOAD_URL="https://nodejs.org/dist/latest/node-$NODE_LATEST-linux-$ARM.tar.gz" # Construct the download URL. echo "Installing Latest Node.js ..." diff --git a/js/defaults.js b/js/defaults.js index 28cc2613..1d81252c 100644 --- a/js/defaults.js +++ b/js/defaults.js @@ -38,6 +38,14 @@ var defaults = { text: "See README for more information." } }, + { + module: "helloworld", + position: "middle_center", + classes: "xsmall", + config: { + text: "If you get this message while your config file is already
created, your config file probably contains an error.
Use a JavaScript linter to validate your file." + } + }, { module: "helloworld", position: "bottom_bar", diff --git a/js/loader.js b/js/loader.js index 2dbd40b2..da75908e 100644 --- a/js/loader.js +++ b/js/loader.js @@ -34,7 +34,15 @@ var Loader = (function() { loadNextModule(); }); } else { - startModules(); + // All modules loaded. Load custom.css + // This is done after all the moduels so we can + // overwrite all the defined styls. + + loadFile('css/custom.css', function() { + // custom.css loaded. Start all modules. + startModules(); + }); + } }; diff --git a/js/logger.js b/js/logger.js index 994fe534..5b2df990 100644 --- a/js/logger.js +++ b/js/logger.js @@ -21,6 +21,27 @@ var Log = (function() { }, error: function(message) { console.error(message); + }, + warn: function(message) { + console.warn(message); + }, + group: function(message) { + console.group(message); + }, + groupCollapsed: function(message) { + console.groupCollapsed(message); + }, + groupEnd: function() { + console.groupEnd(); + }, + time: function(message) { + console.time(message); + }, + timeEnd: function(message) { + console.timeEnd(message); + }, + timeStamp: function(message) { + console.timeStamp(message); } }; })(); diff --git a/modules/README.md b/modules/README.md index 965051b0..b2caa2af 100644 --- a/modules/README.md +++ b/modules/README.md @@ -97,7 +97,7 @@ start: function() { ####`getScripts()` **Should return: Array** -The getScripts method is called to request any additional scripts that need to be loaded. This method should therefor return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.js')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder. +The getScripts method is called to request any additional scripts that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.js')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder. **Example:** ````javascript @@ -117,7 +117,7 @@ getScripts: function() { ####`getStyles()` **Should return: Array** -The getStyles method is called to request any additional scripts that need to be loaded. This method should therefor return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.css')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder. +The getStyles method is called to request any additional stylesheets that need to be loaded. This method should therefore return an array with strings. If you want to return a full path to a file in the module folder, use the `this.file('filename.css')` method. In all cases the loader will only load a file once. It even checks if the file is available in the default vendor folder. **Example:** ````javascript @@ -133,6 +133,22 @@ getStyles: function() { ```` **Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls. +####`getTranslations()` +**Should return: Dictionary** + +The getTranslations method is called to request translation files that need to be loaded. This method should therefore return a dictionary with the files to load, identified by the country's short name. + +**Example:** +````javascript +getTranslations: function() { + return { + en: "translations/en.json", + de: "translations/de.json" + } +} + +```` + ####`getDom()` **Should return:** Dom Object @@ -261,6 +277,25 @@ To show a module, you can call the `show(speed, callback)` method. You can call **Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.
**Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender). +####`this.translate(identifier)` +***identifier* String** - Identifier of the string that should be translated. + +The Magic Mirror contains a convenience wrapper for `l18n`. You can use this to automatically serve different translations for your modules based on the user's `language` configuration. + +**Example:** +````javascript +this.translate("INFO") //Will return a translated string for the identifier INFO +```` + +**Example json file:** +````javascript +{ + "INFO": "Really important information!" +} +```` + +**Note:** Currently there is no fallback if a translation identifier does not exist in one language. Right now you always have to add all identifier to all your translations even if they are not translated yet (see [#191](https://github.com/MichMich/MagicMirror/issues/191)). + ## The Node Helper: node_helper.js @@ -458,4 +493,4 @@ The Magic Mirror contains a convenience wrapper for logging. Currently, this log Log.info('error'); Log.log('log'); Log.error('info'); -``` \ No newline at end of file +```` diff --git a/modules/default/alert/alert.js b/modules/default/alert/alert.js index bff91504..084161ac 100644 --- a/modules/default/alert/alert.js +++ b/modules/default/alert/alert.js @@ -18,7 +18,7 @@ Module.register("alert",{ //Position position: "center", //shown at startup - welcome_message: true, + welcome_message: false, }, getScripts: function() { return ["classie.js", "modernizr.custom.js", "notificationFx.js"]; diff --git a/modules/default/alert/notificationFx.js b/modules/default/alert/notificationFx.js index b74c9d14..ca77208d 100644 --- a/modules/default/alert/notificationFx.js +++ b/modules/default/alert/notificationFx.js @@ -144,7 +144,10 @@ if (ev.target !== self.ntf) return false; this.removeEventListener(animEndEventName, onEndAnimationFn); } - self.options.wrapper.removeChild(this); + + if (this.parentNode === self.options.wrapper) { + self.options.wrapper.removeChild(this); + } }; if (support.animations) { diff --git a/modules/default/calendar/README.md b/modules/default/calendar/README.md index cfe4e2b8..00378263 100644 --- a/modules/default/calendar/README.md +++ b/modules/default/calendar/README.md @@ -112,6 +112,14 @@ The following properties can be configured: + + displayRepeatingCountTitle + Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary")
+ +
Possible values: true or false +
Default value: false + + @@ -154,5 +162,12 @@ config: {
Possible values: See Font Awsome website. + + repeatingCountTitle + The count title for yearly repating events in this calendar.
+
Example:
+ 'Birthday' + + diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 5e315586..2d494499 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -15,6 +15,8 @@ Module.register("calendar",{ maximumNumberOfDays: 365, displaySymbol: true, defaultSymbol: "calendar", // Fontawsome Symbol see http://fontawesome.io/cheatsheet/ + displayRepeatingCountTitle: false, + defaultRepeatingCountTitle: '', maxTitleLength: 25, fetchInterval: 5 * 60 * 1000, // Update every 5 minutes. animationSpeed: 2000, @@ -46,7 +48,8 @@ Module.register("calendar",{ return { en: "translations/en.json", de: "translations/de.json", - nl: "translations/nl.json" + nl: "translations/nl.json", + fr: "translations/fr.json" }; }, @@ -113,8 +116,23 @@ Module.register("calendar",{ eventWrapper.appendChild(symbolWrapper); } - var titleWrapper = document.createElement("td"); - titleWrapper.innerHTML = this.titleTransform(event.title); + var titleWrapper = document.createElement("td"), + repeatingCountTitle = ''; + + + if (this.config.displayRepeatingCountTitle) { + + repeatingCountTitle = this.countTitleForUrl(event.url); + + if(repeatingCountTitle !== '') { + var thisYear = new Date().getFullYear(), + yearDiff = thisYear - event.firstYear; + + repeatingCountTitle = ', '+ yearDiff + '. ' + repeatingCountTitle; + } + } + + titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle; titleWrapper.className = "title bright"; eventWrapper.appendChild(titleWrapper); @@ -239,6 +257,23 @@ Module.register("calendar",{ return this.config.defaultSymbol; }, + /* countTitleForUrl(url) + * Retrieves the name for a specific url. + * + * argument url sting - Url to look for. + * + * return string - The Symbol + */ + countTitleForUrl: function(url) { + for (var c in this.config.calendars) { + var calendar = this.config.calendars[c]; + if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") { + return calendar.repeatingCountTitle; + } + } + + return this.config.defaultRepeatingCountTitle; + }, /* shorten(string, maxLength) * Shortens a sting if it's longer than maxLenthg. diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index ac558e2f..00a17fd9 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -55,14 +55,16 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe if (event.type === "VEVENT") { - //console.log(event); - var startDate = (event.start.length === 8) ? moment(event.start, "YYYYMMDD") : moment(new Date(event.start)); var endDate; if (typeof event.end !== "undefined") { endDate = (event.end.length === 8) ? moment(event.end, "YYYYMMDD") : moment(new Date(event.end)); } else { - endDate = startDate; + if (!isFacebookBirthday) { + endDate = startDate; + } else { + endDate = moment(startDate).add(1, 'days'); + } } // calculate the duration f the event for use with recurring events. @@ -84,14 +86,15 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe title: (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary, startDate: startDate.format("x"), endDate: endDate.format("x"), - fullDayEvent: isFullDayEvent(event) + fullDayEvent: isFullDayEvent(event), + firstYear: event.start.getFullYear() }); } } } else { // console.log("Single event ..."); // Single event. - var fullDayEvent = isFullDayEvent(event); + var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event); var title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary; if (!fullDayEvent && endDate < new Date()) { diff --git a/modules/default/calendar/translations/de.json b/modules/default/calendar/translations/de.json index 2e5de572..cea5588d 100644 --- a/modules/default/calendar/translations/de.json +++ b/modules/default/calendar/translations/de.json @@ -1,7 +1,7 @@ { "TODAY": "Heute" , "TOMORROW": "Morgen" - , "RUNNING": "Noch" + , "RUNNING": "noch" , "LOADING": "Lade Termine …" , "EMPTY": "Keine Termine." } diff --git a/modules/default/calendar/translations/fr.json b/modules/default/calendar/translations/fr.json new file mode 100644 index 00000000..45f48b8b --- /dev/null +++ b/modules/default/calendar/translations/fr.json @@ -0,0 +1,7 @@ +{ + "TODAY": "Aujourd'hui" + , "TOMORROW": "Demain" + , "RUNNING": "Se termine dans" + , "LOADING": "Chargement des RDV …" + , "EMPTY": "Aucun RDV." +} diff --git a/modules/default/calendar/vendor/ical.js/node-ical.js b/modules/default/calendar/vendor/ical.js/node-ical.js index 294908ea..2f6ef3ef 100644 --- a/modules/default/calendar/vendor/ical.js/node-ical.js +++ b/modules/default/calendar/vendor/ical.js/node-ical.js @@ -32,7 +32,7 @@ ical.objectHandlers['END'] = function(val, params, curr, stack){ if (curr.start.length === 8) { var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start); if (comps) { - curr.start = new Date (comps[1], comps[2], comps[3]); + curr.start = new Date (comps[1], comps[2] - 1, comps[3]); } } diff --git a/modules/default/clock/README.md b/modules/default/clock/README.md index 522b1ad2..8808825c 100644 --- a/modules/default/clock/README.md +++ b/modules/default/clock/README.md @@ -47,14 +47,21 @@ The following properties can be configured: showPeriod - Show the period (am/pm) with 12 hour format
+ Show the period (am/pm) with 12 hour format.

Possible values: true or false
Default value: true showPeriodUpper - Show the period (AM/PM) with 12 hour format as uppercase
+ Show the period (AM/PM) with 12 hour format as uppercase.
+
Possible values: true or false +
Default value: false + + + + clockBold + Remove the colon and bold the minutes to make a more modern look.

Possible values: true or false
Default value: false diff --git a/modules/default/clock/clock.js b/modules/default/clock/clock.js index 611170f4..7f76324f 100644 --- a/modules/default/clock/clock.js +++ b/modules/default/clock/clock.js @@ -1,41 +1,34 @@ /* global Log, Module, moment, config */ - /* Magic Mirror * Module: Clock * * By Michael Teeuw http://michaelteeuw.nl * MIT Licensed. */ - Module.register("clock",{ - // Module config defaults. defaults: { timeFormat: config.timeFormat, displaySeconds: true, showPeriod: true, showPeriodUpper: false, + clockBold: false }, - // Define required scripts. getScripts: function() { return ["moment.js"]; }, - // Define start sequence. start: function() { Log.info("Starting module: " + this.name); - // Schedule update interval. var self = this; setInterval(function() { self.updateDom(); }, 1000); - // Set locale. moment.locale(config.language); }, - // Override dom generator. getDom: function() { // Create wrappers. @@ -44,35 +37,36 @@ Module.register("clock",{ var timeWrapper = document.createElement("div"); var secondsWrapper = document.createElement("sup"); var periodWrapper = document.createElement("span"); - // Style Wrappers dateWrapper.className = "date normal medium"; timeWrapper.className = "time bright large light"; secondsWrapper.className = "dimmed"; - // Set content of wrappers. - // The moment().format('h') method has a bug on the Raspberry Pi. + // The moment().format("h") method has a bug on the Raspberry Pi. // So we need to generate the timestring manually. // See issue: https://github.com/MichMich/MagicMirror/issues/181 - var timeString = moment().format('HH:mm'); + if (this.config.clockBold === true) { + var timeString = moment().format("HH[]mm[]"); + } else { + var timeString = moment().format("HH:mm"); + } if (this.config.timeFormat !== 24) { var now = new Date(); var hours = now.getHours() % 12 || 12; - timeString = hours + moment().format(':mm'); + if (this.config.clockBold === true) { + timeString = hours + moment().format("[]mm[]"); + } else { + timeString = hours + moment().format(":mm"); + } } - dateWrapper.innerHTML = moment().format("dddd, LL"); timeWrapper.innerHTML = timeString; secondsWrapper.innerHTML = moment().format("ss"); - - if (this.config.showPeriodUpper) { - periodWrapper.innerHTML = moment().format('A'); + periodWrapper.innerHTML = moment().format("A"); } else { - periodWrapper.innerHTML = moment().format('a'); + periodWrapper.innerHTML = moment().format("a"); } - - // Combine wrappers. wrapper.appendChild(dateWrapper); wrapper.appendChild(timeWrapper); @@ -82,7 +76,6 @@ Module.register("clock",{ if (this.config.showPeriod && this.config.timeFormat !== 24) { timeWrapper.appendChild(periodWrapper); } - // Return the wrapper to the dom. return wrapper; } diff --git a/modules/default/weatherforecast/README.md b/modules/default/weatherforecast/README.md index c5be33ce..9e93bd14 100644 --- a/modules/default/weatherforecast/README.md +++ b/modules/default/weatherforecast/README.md @@ -55,6 +55,14 @@ The following properties can be configured:
Default value: config.units + + maxNumberOfDays + How many days of forecast to return. Specified by config.js
+
Possible values: 1 - 16 +
Default value: 7 (7 days) +
This value is optional. By default the weatherforecast module will return 7 days. + + updateInterval How often does the content needs to be fetched? (Milliseconds)
diff --git a/modules/default/weatherforecast/weatherforecast.js b/modules/default/weatherforecast/weatherforecast.js index c1e79828..c5b8762b 100644 --- a/modules/default/weatherforecast/weatherforecast.js +++ b/modules/default/weatherforecast/weatherforecast.js @@ -14,6 +14,7 @@ Module.register("weatherforecast",{ location: "", appid: "", units: config.units, + maxNumberOfDays: 7, updateInterval: 10 * 60 * 1000, // every 10 minutes animationSpeed: 1000, timeFormat: config.timeFormat, @@ -189,6 +190,12 @@ Module.register("weatherforecast",{ params += "q=" + this.config.location; params += "&units=" + this.config.units; params += "&lang=" + this.config.lang; + /* + * Submit a specific number of days to forecast, between 1 to 16 days. + * The OpenWeatherMap API properly handles values outside of the 1 - 16 range and returns 7 days by default. + * This is simply being pedantic and doing it ourselves. + */ + params += "&cnt=" + (((this.config.maxNumberOfDays < 1) || (this.config.maxNumberOfDays > 16)) ? 7 : this.config.maxNumberOfDays); params += "&APPID=" + this.config.appid; return params;