Add module subfolder support.

This commit is contained in:
Michael Teeuw
2016-04-01 17:35:29 +02:00
parent 671338425a
commit e4c54cc655
27 changed files with 56 additions and 21 deletions

View File

@@ -0,0 +1,152 @@
# Module: Calendar
The `calendar` module is one of the default modules of the MagicMirror.
This module displays events from a public .ical calendar. It can combine multiple calendars.
## Using the module
To use this module, add it to the modules array in the `config/config.js` file:
````javascript
modules: [
{
module: 'calendar',
position: 'top_left', // This can be any of the regions. Best results in left or right regions.
config: {
// The config property is optional.
// If no config is set, an example calendar is shown.
// See 'Configuration options' for more information.
}
}
]
````
## Configuration options
The following properties can be configured:
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>maximumEntries</code></td>
<td>The maximum number of events shown.<br>
<br><b>Possible values:</b> <code>0</code> - <code>100</code>
<br><b>Default value:</b> <code>10</code>
</td>
</tr>
<tr>
<td><code>displaySymbol</code></td>
<td>Display a symbol in front of an entry.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>defaultSymbol</code></td>
<td>The default symbol.<br>
<br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awsome</a> website.
<br><b>Default value:</b> <code>calendar</code>
</td>
</tr>
<tr>
<td><code>maxTitleLength</code></td>
<td>The maximum title length.<br>
<br><b>Possible values:</b> <code>10</code> - <code>50</code>
<br><b>Default value:</b> <code>25</code>
</td>
</tr>
<tr>
<td><code>fetchInterval</code></td>
<td>How often does the content needs to be fetched? (Milliseconds)<br>
<br><b>Possible values:</b> <code>1000</code> - <code>86400000</code>
<br><b>Default value:</b> <code>300000</code> (5 minutes)
</td>
</tr>
<tr>
<td><code>animationSpeed</code></td>
<td>Speed of the update animation. (Milliseconds)<br>
<br><b>Possible values:</b><code>0</code> - <code>5000</code>
<br><b>Default value:</b> <code>2000</code> (2 seconds)
</td>
</tr>
<tr>
<td><code>fade</code></td>
<td>Fade the future events to black. (Gradient)<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>fadePoint</code></td>
<td>Where to start fade?<br>
<br><b>Possible values:</b> <code>0</code> (top of the list) - <code>1</code> (bottom of list)
<br><b>Default value:</b> <code>0.25</code>
</td>
</tr>
<tr>
<td><code>calendars</code></td>
<td>The list of calendars.<br>
<br><b>Possible values:</b> An array, see <i>calendar configuration</i> below.
<br><b>Default value:</b> <i>An example calendar.</i>
</td>
</tr>
<tr>
<td><code>titleReplace</code></td>
<td>An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title.<br>
<br><b>Example:</b> <br>
<code>
titleReplace: {'Birthday of ' : '', 'foo':'bar'}
</code>
</td>
</tr>
</tbody>
</table>
### Calendar configuration
The `calendars` property contains an array of the configured calendars.
#### Default value:
````javascript
config: {
calendars: [
{
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
symbol: 'calendar',
},
],
}
````
#### Calendar configuration options:
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>The url of the calendar .ical. This property is required.<br>
<br><b>Possible values:</b> Any public accessble .ical calendar.
</td>
</tr>
<tr>
<td><code> symbol </code></td>
<td>The symbol to show in front of an event. This property is optional.<br>
<br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awsome</a> website.
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,23 @@
.calendar .symbol {
padding-left: 0px;
padding-right: 10px;
font-size: 80%;
}
.calendar .symbol span {
display: inline-block;
-ms-transform: translate(0px,2px); /* IE 9 */
-webkit-transform: translate(0px,2px); /* Safari */
transform: translate(0px,2px);
}
.calendar .title {
padding-left: 0px;
padding-right: 0px;
}
.calendar .time {
padding-left: 30px;
text-align: right;
}

View File

@@ -0,0 +1,240 @@
/* global Module */
/* Magic Mirror
* Module: Calendar
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*/
Module.register('calendar',{
// Define module defaults
defaults: {
maximumEntries: 10, // Total Maximum Entries
displaySymbol: true,
defaultSymbol: 'calendar', // Fontawsome Symbol see http://fontawesome.io/cheatsheet/
maxTitleLength: 25,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
calendars: [
{
symbol: 'calendar',
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
},
],
titleReplace: {
'De verjaardag van ' : ''
}
},
// Define required scripts.
getStyles: function() {
return ['calendar.css', 'font-awesome.css'];
},
// Define required scripts.
getScripts: function() {
return ['moment.js'];
},
// Override start method.
start: function() {
Log.log('Starting module: ' + this.name);
// Set locale.
moment.locale(config.language);
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace('webcal://', 'http://');
this.addCalendar(calendar.url);
}
this.calendarData = {};
},
// Override socket notification handler.
socketNotificationReceived: function(notification, payload) {
if (notification === 'CALENDAR_EVENTS') {
if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events;
}
} else if(notification === 'FETCH_ERROR') {
Log.error('Calendar Error. Could not fetch calendar: ' + payload.url);
} else if(notification === 'INCORRECT_URL') {
Log.error('Calendar Error. Incorrect url: ' + payload.url);
} else {
Log.log('Calendar received an unknown socket notification: '+notification);
}
this.updateDom(this.config.animationSpeed);
},
// Override dom generator.
getDom: function() {
var events = this.createEventList();
var wrapper = document.createElement("table");
wrapper.className = "small";
if (events.length === 0) {
wrapper.innerHTML = "Loading events ...";
wrapper.className = "small dimmed";
return wrapper;
}
for (var e in events) {
var event = events[e];
var eventWrapper = document.createElement("tr");
eventWrapper.className = "normal";
if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td");
symbolWrapper.className = "symbol";
var symbol = document.createElement("span");
symbol.className = "fa fa-" + this.symbolForUrl(event.url);
symbolWrapper.appendChild(symbol);
eventWrapper.appendChild(symbolWrapper);
}
var titleWrapper = document.createElement("td");
titleWrapper.innerHTML = this.titleTransform(event.title);
titleWrapper.className = "title bright";
eventWrapper.appendChild(titleWrapper);
var timeWrapper = document.createElement("td");
timeWrapper.innerHTML = moment(event.startDate,'x').fromNow();
timeWrapper.className = "time light";
eventWrapper.appendChild(timeWrapper);
wrapper.appendChild(eventWrapper);
// Create fade effect.
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startingPoint = events.length * this.config.fadePoint;
var steps = events.length - startingPoint;
if (e >= startingPoint) {
var currentStep = e - startingPoint;
eventWrapper.style.opacity = 1 - (1 / steps * currentStep);
}
}
}
return wrapper;
},
/* hasCalendarURL(url)
* Check if this config contains the calendar url.
*
* argument url sting - Url to look for.
*
* return bool - Has calendar url
*/
hasCalendarURL: function(url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url) {
return true;
}
}
return false;
},
/* createEventList()
* Creates the sorted list of all events.
*
* return array - Array with events.
*/
createEventList: function() {
var events = [];
for (var c in this.calendarData) {
var calendar = this.calendarData[c];
for (var e in calendar) {
var event = calendar[e];
event.url = c;
events.push(event);
}
}
events.sort(function(a,b) {
return a.startDate - b.startDate;
});
return events.slice(0, this.config.maximumEntries);
},
/* createEventList(url)
* Requests node helper to add calendar url.
*
* argument url sting - Url to add.
*/
addCalendar: function(url) {
this.sendSocketNotification('ADD_CALENDAR', {
url: url,
maximumEntries: this.config.maximumEntries,
fetchInterval: this.config.fetchInterval
});
},
/* symbolForUrl(url)
* Retrieves the symbol for a specific url.
*
* argument url sting - Url to look for.
*
* return string - The Symbol
*/
symbolForUrl: function(url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.symbol === 'string') {
return calendar.symbol;
}
}
return this.config.defaultSymbol;
},
/* shorten(string, maxLength)
* Shortens a sting if it's longer than maxLenthg.
* Adds an ellipsis to the end.
*
* argument string string - The string to shorten.
* argument maxLength number - The max lenth of the string.
*
* return string - The shortened string.
*/
shorten: function(string, maxLength) {
if (string.length > maxLength) {
return string.slice(0,maxLength) + "&hellip;";
}
return string;
},
/* titleTransform(title)
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength
*
* argument title string - The title to transform.
*
* return string - The transformed title.
*/
titleTransform: function(title) {
for (var needle in this.config.titleReplace) {
var replacement = this.config.titleReplace[needle];
title = title.replace(needle, replacement);
}
title = this.shorten(title, this.config.maxTitleLength);
return title;
}
});

View File

@@ -0,0 +1,242 @@
/* Magic Mirror
* Node Helper: Calendar
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*/
var NodeHelper = require('node_helper');
var ical = require('ical');
var moment = require('moment');
var validUrl = require('valid-url');
var CalendarFetcher = function(url, reloadInterval, maximumEntries) {
var self = this;
var reloadTimer = null;
var events = [];
var fetchFailedCallback = function() {};
var eventsReceivedCallback = function() {};
/* fetchCalendar()
* Initiates calendar fetch.
*/
var fetchCalendar = function() {
clearTimeout(reloadTimer);
reloadTimer = null;
ical.fromURL(url, {}, function(err, data) {
if (err) {
fetchFailedCallback(self, err);
scheduleTimer();
return;
}
//console.log(data);
newEvents = [];
var limitFunction = function (date, i){return i < maximumEntries;};
for (var e in data) {
var event = data[e];
if (event.type === 'VEVENT') {
var startDate = (event.start.length === 8) ? moment(event.start, 'YYYYMMDD') : moment(new Date(event.start));
if (typeof event.rrule != 'undefined') {
var rule = event.rrule;
// Check if the timeset is set to this current time.
// If so, the RRULE line does not contain any BYHOUR, BYMINUTE, BYSECOND params.
// This causes the times of the recurring event to be incorrect.
// By adjusting the timeset property, this issue is solved.
var now = new Date();
if (rule.timeset[0].hour == now.getHours(),
rule.timeset[0].minute == now.getMinutes(),
rule.timeset[0].second == now.getSeconds()) {
rule.timeset[0].hour = startDate.format('H');
rule.timeset[0].minute = startDate.format('m');
rule.timeset[0].second = startDate.format('s');
}
var oneYear = new Date();
oneYear.setFullYear(oneYear.getFullYear() + 1);
var dates = rule.between(new Date(), oneYear, true, limitFunction);
//console.log(dates);
for (var d in dates) {
startDate = moment(new Date(dates[d]));
newEvents.push({
title: event.summary,
startDate: startDate.format('x')
});
}
} else {
// Single event.
var today = moment().startOf('day');
if (startDate > today) {
newEvents.push({
title: event.summary,
startDate: startDate.format('x')
});
}
}
}
}
newEvents.sort(function(a,b) {
return a.startDate - b.startDate;
});
events = newEvents.slice(0, maximumEntries);
self.broadcastEvents();
scheduleTimer();
});
};
/* scheduleTimer()
* Schedule the timer for the next update.
*/
var scheduleTimer = function() {
//console.log('Schedule update timer.');
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function() {
fetchCalendar();
}, reloadInterval);
};
/* public methods */
/* startFetch()
* Initiate fetchCalendar();
*/
this.startFetch = function() {
fetchCalendar();
};
/* broadcastItems()
* Broadcast the exsisting events.
*/
this.broadcastEvents = function() {
if (events.length <= 0) {
//console.log('No events to broadcast yet.');
return;
}
//console.log('Broadcasting ' + events.length + ' events.');
eventsReceivedCallback(self);
};
/* onReceive(callback)
* Sets the on success callback
*
* argument callback function - The on success callback.
*/
this.onReceive = function(callback) {
eventsReceivedCallback = callback;
};
/* onError(callback)
* Sets the on error callback
*
* argument callback function - The on error callback.
*/
this.onError = function(callback) {
fetchFailedCallback = callback;
};
/* url()
* Returns the url of this fetcher.
*
* return string - The url of this fetcher.
*/
this.url = function() {
return url;
};
/* events()
* Returns current available events for this fetcher.
*
* return array - The current available events for this fetcher.
*/
this.events = function() {
return events;
};
};
module.exports = NodeHelper.create({
// Override start method.
start: function() {
var self = this;
var events = [];
this.fetchers = [];
console.log('Starting node helper for: ' + this.name);
},
// Override socketNotificationReceived method.
socketNotificationReceived: function(notification, payload) {
if (notification === 'ADD_CALENDAR') {
//console.log('ADD_CALENDAR: ');
this.createFetcher(payload.url, payload.fetchInterval, payload.maximumEntries);
}
},
/* createFetcher(url, reloadInterval)
* Creates a fetcher for a new url if it doesn't exsist yet.
* Otherwise it reuses the exsisting one.
*
* attribute url string - URL of the news feed.
* attribute reloadInterval number - Reload interval in milliseconds.
*/
createFetcher: function(url, fetchInterval, maximumEntries) {
var self = this;
if (!validUrl.isUri(url)){
self.sendSocketNotification('INCORRECT_URL', {url:url});
return;
}
var fetcher;
if (typeof self.fetchers[url] === 'undefined') {
console.log('Create new calendar fetcher for url: ' + url + ' - Interval: ' + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries);
fetcher.onReceive(function(fetcher) {
//console.log('Broadcast events.');
//console.log(fetcher.events());
self.sendSocketNotification('CALENDAR_EVENTS', {
url: fetcher.url(),
events: fetcher.events()
});
});
fetcher.onError(function(fetcher, error) {
self.sendSocketNotification('FETCH_ERROR', {
url: fetcher.url(),
error: error
});
});
self.fetchers[url] = fetcher;
} else {
//console.log('Use exsisting news fetcher for url: ' + url);
fetcher = self.fetchers[url];
fetcher.broadcastEvents();
}
fetcher.startFetch();
}
});