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;