Merge pull request #586 from MichMich/develop

Release 2.1.0
This commit is contained in:
Michael Teeuw 2016-12-31 21:17:54 +01:00 committed by GitHub
commit 8010e6220d
74 changed files with 3291 additions and 763 deletions

View File

@ -4,11 +4,13 @@
"quotes": ["error", "double"],
"max-len": ["error", 250],
"curly": "error",
"camelcase": ["error", {"properties": "never"}]
"camelcase": ["error", {"properties": "never"}],
"no-trailing-spaces": ["error"],
"no-irregular-whitespace": ["error"]
},
"env": {
"browser": true,
"node": true,
"es6": true
}
}
}

View File

@ -23,7 +23,7 @@ To run StyleLint, use `grunt stylelint`.
### Submitting Issues
Please only submit reproducible issues.
Please only submit reproducible issues.
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)

View File

@ -1,4 +1,4 @@
> Please send your pull requests the develop branch.
> Please send your pull requests the develop branch.
> Don't forget to add the change to CHANGELOG.md.
* Does the pull request solve a **related** issue?

View File

@ -2,13 +2,65 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.1.0] - 2016-12-31
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Added
- Finnish translation.
- Danish translation.
- Turkish translation.
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MichMich/MagicMirror/issues/456)).
- Added ability to change the point of time when calendar events get relative.
- Add Splash screen on boot.
- Add option to show humidity in currentWeather module.
- Add VSCode IntelliSense support.
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information.
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
- Module API: Option to define the minimum MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
- Added option to show rain amount in the weatherforecast default module
- Add module `updatenotification` to get an update whenever a new version is availabe. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
- Add the abilty to set timezone on the date display in the Clock Module
- Ability to set date format in calendar module
- Possibility to use currentweather for the compliments
- Added option `disabled` for modules.
- Added option `address` to set bind address.
- Added option `onlyTemp` for currentweather module to show show only current temperature and weather icon.
- Added option `remoteFile` to compliments module to load compliment array from filesystem.
- Added option `zoom` to scale the whole mirror display with a given factor.
- Added option `roundTemp` for currentweather and weatherforecast modules to display temperatures rounded to nearest integer.
- Added abilty set the classes option to compliments module for style and text size of compliments.
- Added ability to configure electronOptions
- Calendar module: option to hide private events
- Add root_path for global vars
### Updated
- Modified translations for Frysk.
- Modified core English translations.
- Updated package.json as a result of Snyk security update.
- Improve object instantiation to prevent reference errors.
- Improve logger. `Log.log()` now accepts multiple arguments.
- Remove extensive logging in newsfeed node helper.
- Calendar times are now uniformly capitalized.
- Modules are now secure, and Helmet is now used to prevent abuse of the Mirror's API.
### Fixed
- Solve an issue where module margins would appear when the first module of a section was hidden.
- Solved visual display errors on chrome, if all modules in one of the right sections are hidden.
- Global and Module default config values are no longer modified when setting config values.
- Hide a region if all modules in a region are hidden. Prevention unwanted margins.
- Replaced `electron-prebuilt` package with `electron` in order to fix issues that would happen after 2017.
- Documentation of alert module
## [2.0.5] - 2016-09-20
### Added
- Added ability to remove tags from the beginning or end of newsfeed items in 'newsfeed.js'.
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
- Added CII Badge (we are compliant with the CII Best Practices)
- Add support for doing http basic auth when loading calendars
- Add support for doing http basic auth when loading calendars
- Add the abilty to turn off and on the date display in the Clock Module
### Fixed

View File

@ -6,7 +6,10 @@ module.exports = function(grunt) {
options: {
configFile: ".eslintrc.json"
},
target: ["js/*.js", "modules/default/*.js", "serveronly/*.js", "*.js"]
target: ["js/*.js", "modules/default/*.js", "modules/default/*/*.js",
"serveronly/*.js", "*.js", "!modules/default/alert/notificationFx.js",
"!modules/default/alert/modernizr.custom.js", "!modules/default/alert/classie.js"
]
},
stylelint: {
simple: {
@ -47,7 +50,6 @@ module.exports = function(grunt) {
"MD018": false,
"MD012": false,
"MD026": false,
"MD036": false,
"MD038": false
}
},
@ -64,4 +66,4 @@ module.exports = function(grunt) {
grunt.loadNpmTasks("grunt-yamllint");
grunt.loadNpmTasks("grunt-markdownlint");
grunt.registerTask("default", ["eslint", "stylelint", "jsonlint", "markdownlint", "yamllint"]);
};
};

View File

@ -60,7 +60,7 @@ The following wiki links are helpful in the configuration of your MagicMirror²
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
```bash
git pull
git pull && npm install
```
If you changed nothing more than the config or the modules, this should work without any problems.
@ -73,15 +73,18 @@ Type `git status` to see your changes, if there are any, you can reset them with
The following properties can be configured:
| **Option** | **Description** |
| --- | --- |
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
| `kioskmode` | This allows MagicMirror² to run in Kiosk Mode. It protects from other programs popping on top of your screen. The default value is `false`|
| `address` | The ip address the accept connections. The default open bind `::` is IPv6 is available or `0.0.0.0` IPv4 run on. Example config: `192.168.10.100`. |
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`).|
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
| `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. |
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (defaults `.width = 800` & `.height = 600`). Kiosk mode can be enabled by setting `.kiosk = true`, `.autoHideMenuBar = false`, `.fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
Module configuration:
@ -91,6 +94,7 @@ Module configuration:
| `position` | The location of the module in which the module will be loaded. Possible values are `top_ bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. |
| `classes` | Additional classes which are passed to the module. The field is optional. |
| `header` | To display a header text above the module, add the header property. This field is optional. |
| `disabled` | Set disabled to `true` to skip creating the module. This field is optional. |
| `config` | An object with the module configuration properties. Check the documentation of the module for more information. This field is optional, unless the module requires extra configuration. |
## Modules
@ -129,3 +133,8 @@ Please keep the following in mind:
- **New Features**: please please discuss in a GitHub issue before you start to alter a big part of the code. Without discussion upfront, the pull request will not be accepted / merged.
Thanks for your help in making MagicMirror² better!
<p align="center">
<br>
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
</p>

View File

@ -6,6 +6,7 @@
var config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: 'en',
timeFormat: 24,
@ -15,6 +16,10 @@ var config = {
{
module: 'alert',
},
{
module: "updatenotification",
position: "top_bar"
},
{
module: 'clock',
position: 'top_left'
@ -50,9 +55,9 @@ var config = {
position: 'top_right',
header: 'Weather Forecast',
config: {
location: 'New York',
location: 'New York',
locationID: '5128581', //ID from http://www.openweathermap.org
appid: 'YOUR_OPENWEATHER_API_KEY'
appid: 'YOUR_OPENWEATHER_API_KEY'
}
},
{

View File

@ -113,11 +113,12 @@ sup {
*/
.module {
margin-top: 30px;
margin-bottom: 30px;
}
.module:first-child {
margin-top: 0;
.region.bottom .module {
margin-top: 30px;
margin-bottom: 0;
}
/**

View File

@ -8,6 +8,10 @@
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
var version = "#VERSION#";
</script>
</head>
<body>
<div class="region fullscreen below"><div class="container"></div></div>

View File

@ -18,7 +18,7 @@ echo ' \$$$$$$ |'
echo ' \______/'
echo -e "\e[0m"
# Define the tested version of Node.js.
# Define the tested version of Node.js.
NODE_TESTED="v5.1.0"
#Determine which Pi is running.
@ -49,19 +49,19 @@ if command_exists node; then
echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\e[0m"
echo -e "\e[0mInstalled Node version: \e[1m$NODE_CURRENT\e[0m"
if version_gt $NODE_TESTED $NODE_CURRENT; then
echo -e "\e[96mNode should be upgraded.\e[0m"
NODE_INSTALL=true
echo -e "\e[96mNode should be upgraded.\e[0m"
NODE_INSTALL=true
#Check if a node process is currenlty running.
#If so abort installation.
if pgrep "node" > /dev/null; then
echo -e "\e[91mA Node process is currently running. Can't upgrade."
echo "Please quit all Node processes and restart the installer."
exit;
#Check if a node process is currenlty running.
#If so abort installation.
if pgrep "node" > /dev/null; then
echo -e "\e[91mA Node process is currently running. Can't upgrade."
echo "Please quit all Node processes and restart the installer."
exit;
fi
else
echo -e "\e[92mNo Node.js upgrade nessecery.\e[0m"
else
echo -e "\e[92mNo Node.js upgrade nessecery.\e[0m"
fi
else
@ -113,6 +113,34 @@ else
exit;
fi
# Check if plymouth is installed (default with PIXEL desktop environment), then install custom splashscreen.
echo -e "\e[96mCheck plymouth installation ...\e[0m"
if command_exists plymouth; then
THEME_DIR="/usr/share/plymouth/themes"
echo -e "\e[90mSplashscreen: Checking themes directory.\e[0m"
if [ -d $THEME_DIR ]; then
echo -e "\e[90mSplashscreen: Create theme directory if not exists.\e[0m"
if [ ! -d $THEME_DIR/MagicMirror ]; then
sudo mkdir $THEME_DIR/MagicMirror
fi
if sudo cp ~/MagicMirror/splashscreen/splash.png $THEME_DIR/MagicMirror/splash.png && sudo cp ~/MagicMirror/splashscreen/MagicMirror.plymouth $THEME_DIR/MagicMirror/MagicMirror.plymouth && sudo cp ~/MagicMirror/splashscreen/MagicMirror.script $THEME_DIR/MagicMirror/MagicMirror.script; then
echo -e "\e[90mSplashscreen: Theme copied successfully.\e[0m"
if sudo plymouth-set-default-theme -R MagicMirror; then
echo -e "\e[92mSplashscreen: Changed theme to MagicMirror successfully.\e[0m"
else
echo -e "\e[91mSplashscreen: Couldn't change theme to MagicMirror!\e[0m"
fi
else
echo -e "\e[91mSplashscreen: Copying theme failed!\e[0m"
fi
else
echo -e "\e[91mSplashscreen: Themes folder doesn't exist!\e[0m"
fi
else
echo -e "\e[93mplymouth is not installed.\e[0m";
fi
echo " "
echo -e "\e[92mWe're ready! Run \e[1m\e[97mDISPLAY=:0 npm start\e[0m\e[92m from the ~/MagicMirror directory to start your MagicMirror.\e[0m"
echo " "

View File

@ -10,6 +10,13 @@ var Server = require(__dirname + "/server.js");
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
var path = require("path");
// Get version number.
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
console.log("Starting MagicMirror: v" + global.version);
// global absolute root path
global.root_path = path.resolve(__dirname + "/../");
// The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better.
process.on("uncaughtException", function (err) {
@ -34,7 +41,7 @@ var App = function() {
var loadConfig = function(callback) {
console.log("Loading config ...");
var defaults = require(__dirname + "/defaults.js");
var configFilename = path.resolve(__dirname + "/../config/config.js");
var configFilename = path.resolve(global.root_path + "/config/config.js");
try {
fs.accessSync(configFilename, fs.F_OK);
var c = require(configFilename);
@ -82,6 +89,17 @@ var App = function() {
if (loadModule) {
var Module = require(helperPath);
var m = new Module();
if (m.requiresVersion) {
console.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
console.log("Version is ok!");
} else {
console.log("Version is incorrect. Skip module: '" + moduleName + "'");
return;
}
}
m.setName(moduleName);
m.setPath(path.resolve(moduleFolder));
nodeHelpers.push(m);
@ -103,6 +121,28 @@ var App = function() {
console.log("All module helpers loaded.");
};
/* cmpVersions(a,b)
* Compare two symantic version numbers and return the difference.
*
* argument a string - Version number a.
* argument a string - Version number b.
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}
}
return segmentsA.length - segmentsB.length;
}
/* start(callback)
* This methods starts the core app.
* It loads the config, then it loads all modules.
@ -119,7 +159,7 @@ var App = function() {
for (var m in config.modules) {
var module = config.modules[m];
if (modules.indexOf(module.module) === -1) {
if (modules.indexOf(module.module) === -1 && !module.disabled) {
modules.push(module.module);
}
}
@ -147,4 +187,4 @@ var App = function() {
};
};
module.exports = new App();
module.exports = new App();

View File

@ -21,28 +21,32 @@
var prototype = new this();
initializing = false;
// Make a copy of all prototype properies, to prevent reference issues.
for (var name in prototype) {
prototype[name] = cloneObject(prototype[name]);
}
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn) {
return function() {
var tmp = this._super;
typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) {
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
return ret;
};
})(name, prop[name]) : prop[name];
}
// The dummy class constructor
@ -66,5 +70,24 @@ prop[name];
};
})();
//Define the clone method for later use.
//Helper Method
function cloneObject(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
var temp = obj.constructor(); // give temp the original obj's constructor
for (var key in obj) {
temp[key] = cloneObject(obj[key]);
if (key === "lockStrings") {
Log.log(key);
}
}
return temp;
}
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = Class;}

View File

@ -10,12 +10,19 @@
var defaults = {
port: 8080,
kioskmode: false,
electronOptions: {},
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
zoom: 1,
modules: [
{
module: "updatenotification",
position: "top_center"
},
{
module: "helloworld",
position: "upper_third",

View File

@ -18,13 +18,33 @@ const BrowserWindow = electron.BrowserWindow;
let mainWindow;
function createWindow() {
// Create the browser window.
if (config.kioskmode) {
mainWindow = new BrowserWindow({width: 800, height: 600, x: 0, y: 0, kiosk:true, darkTheme: true, webPreferences: {nodeIntegration: false}});
} else {
mainWindow = new BrowserWindow({width: 800, height: 600, x: 0, y: 0, fullscreen: true, autoHideMenuBar: true, darkTheme: true, webPreferences: {nodeIntegration: false}});
var electronOptionsDefaults = {
width: 800,
height: 600,
x: 0,
y: 0,
darkTheme: true,
webPreferences: {
nodeIntegration: false,
zoomFactor: config.zoom
}
}
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
// settings these options directly instead provides cleaner interface
if (config.kioskmode) {
electronOptionsDefaults.kiosk = true;
} else {
electronOptionsDefaults.fullscreen = true;
electronOptionsDefaults.autoHideMenuBar = true;
}
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
// Create the browser window.
mainWindow = new BrowserWindow(electronOptions);
// and load the index.html of the app.
//mainWindow.loadURL('file://' + __dirname + '../../index.html');
mainWindow.loadURL("http://localhost:" + config.port);

View File

@ -89,6 +89,10 @@ var Loader = (function() {
moduleFolder = config.paths.modules + "/default/" + module;
}
if (moduleData.disabled === true) {
continue;
}
moduleFiles.push({
index: m,
identifier: "module_" + m + "_" + module,
@ -117,9 +121,13 @@ var Loader = (function() {
var afterLoad = function() {
var moduleObject = Module.create(module.name);
bootstrapModule(module, moduleObject, function() {
if (moduleObject) {
bootstrapModule(module, moduleObject, function() {
callback();
});
} else {
callback();
});
}
};
if (loadedModuleFiles.indexOf(url) !== -1) {

View File

@ -13,35 +13,15 @@
var Log = (function() {
return {
info: function(message) {
console.info(message);
},
log: function(message) {
console.log(message);
},
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);
}
info: Function.prototype.bind.call(console.info, console),
log: Function.prototype.bind.call(console.log, console),
error: Function.prototype.bind.call(console.error, console),
warn: Function.prototype.bind.call(console.warn, console),
group: Function.prototype.bind.call(console.group, console),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
time: Function.prototype.bind.call(console.time, console),
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
};
})();

View File

@ -40,6 +40,7 @@ var MM = (function() {
if (typeof module.data.header !== "undefined" && module.data.header !== "") {
var moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.data.header;
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader);
}
@ -51,6 +52,8 @@ var MM = (function() {
}
}
updateWrapperStates();
sendNotification("DOM_OBJECTS_CREATED");
};
@ -73,7 +76,7 @@ var MM = (function() {
/* sendNotification(notification, payload, sender)
* Send a notification to all modules.
*
* argument notification string - The identifier of the noitication.
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
* argument sender Module - The module that sent the notification.
*/
@ -94,26 +97,27 @@ var MM = (function() {
*/
var updateDom = function(module, speed) {
var newContent = module.getDom();
var newHeader = module.getHeader();
if (!module.hidden) {
if (!moduleNeedsUpdate(module, newContent)) {
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
return;
}
if (!speed) {
updateModuleContent(module, newContent);
updateModuleContent(module, newHeader, newContent);
return;
}
hideModule(module, speed / 2, function() {
updateModuleContent(module, newContent);
updateModuleContent(module, newHeader, newContent);
if (!module.hidden) {
showModule(module, speed / 2);
}
});
} else {
updateModuleContent(module, newContent);
updateModuleContent(module, newHeader, newContent);
}
};
@ -125,14 +129,23 @@ var MM = (function() {
*
* return bool - Does the module need an update?
*/
var moduleNeedsUpdate = function(module, newContent) {
var moduleNeedsUpdate = function(module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
var contentWrapper = moduleWrapper.getElementsByClassName("module-content")[0];
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var tempWrapper = document.createElement("div");
tempWrapper.appendChild(newContent);
var headerNeedsUpdate = false;
var contentNeedsUpdate = false;
return tempWrapper.innerHTML !== contentWrapper.innerHTML;
if (headerWrapper.length > 0) {
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
}
var tempContentWrapper = document.createElement("div");
tempContentWrapper.appendChild(newContent);
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
return headerNeedsUpdate || contentNeedsUpdate;
};
/* moduleNeedsUpdate(module, newContent)
@ -141,12 +154,19 @@ var MM = (function() {
* argument module Module - The module to check.
* argument newContent Domobject - The new content that is generated.
*/
var updateModuleContent = function(module, content) {
var updateModuleContent = function(module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
var contentWrapper = moduleWrapper.getElementsByClassName("module-content")[0];
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
contentWrapper[0].innerHTML = "";
contentWrapper[0].appendChild(newContent);
if( headerWrapper.length > 0 && newHeader) {
headerWrapper[0].innerHTML = newHeader;
}
contentWrapper.innerHTML = "";
contentWrapper.appendChild(content);
};
/* hideModule(module, speed, callback)
@ -156,7 +176,17 @@ var MM = (function() {
* argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done.
*/
var hideModule = function(module, speed, callback) {
var hideModule = function(module, speed, callback, options) {
options = 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);
}
}
var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
@ -165,10 +195,12 @@ var MM = (function() {
clearTimeout(module.showHideTimer);
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
// 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 = "absolute";
moduleWrapper.style.position = "fixed";
updateWrapperStates();
if (typeof callback === "function") { callback(); }
}, speed);
@ -182,7 +214,30 @@ var MM = (function() {
* argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done.
*/
var showModule = function(module, speed, callback) {
var showModule = function(module, speed, callback, options) {
options = options || {};
// remove lockString if set in options.
if (options.lockString) {
var 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(","));
return;
}
// If forced show, clean current lockstrings.
if (module.lockStrings.length !== 0 && options.force === true) {
Log.log("Force show of module: " + module.name);
module.lockStrings = [];
}
var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
@ -190,6 +245,8 @@ var MM = (function() {
moduleWrapper.style.position = "static";
moduleWrapper.style.opacity = 1;
updateWrapperStates();
clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function() {
if (typeof callback === "function") { callback(); }
@ -198,6 +255,36 @@ var MM = (function() {
}
};
/* updateWrapperStates()
* Checks for all positions if it has visible content.
* If not, if will hide the position to prevent unwanted margins.
* This method schould 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.
*/
var updateWrapperStates = function() {
var 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"];
positions.forEach(function(position) {
var wrapper = selectWrapper(position);
var moduleWrappers = wrapper.getElementsByClassName("module");
var 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";
});
};
/* loadConfig()
* Loads the core config and combines it with de system defaults.
*/
@ -208,7 +295,7 @@ var MM = (function() {
return;
}
config = Object.assign(defaults, config);
config = Object.assign({}, defaults, config);
};
/* setSelectionMethodsForModules()
@ -221,7 +308,7 @@ var MM = (function() {
/* withClass(className)
* filters a collection of modules based on classname(s).
*
* argument className string/array - one or multiple classnames. (array or space devided)
* argument className string/array - one or multiple classnames. (array or space divided)
*
* return array - Filtered collection of modules.
*/
@ -251,7 +338,7 @@ var MM = (function() {
/* exceptWithClass(className)
* filters a collection of modules based on classname(s). (NOT)
*
* argument className string/array - one or multiple classnames. (array or space devided)
* argument className string/array - one or multiple classnames. (array or space divided)
*
* return array - Filtered collection of modules.
*/
@ -401,10 +488,11 @@ var MM = (function() {
* argument module Module - The module hide.
* argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/
hideModule: function(module, speed, callback) {
hideModule: function(module, speed, callback, options) {
module.hidden = true;
hideModule(module, speed, callback);
hideModule(module, speed, callback, options);
},
/* showModule(module, speed, callback)
@ -413,10 +501,11 @@ var MM = (function() {
* argument module Module - The module show.
* argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/
showModule: function(module, speed, callback) {
showModule: function(module, speed, callback, options) {
module.hidden = false;
showModule(module, speed, callback);
showModule(module, speed, callback, options);
}
};

View File

@ -14,23 +14,30 @@ var Module = Class.extend({
* All methods (and properties) below can be subclassed. *
*********************************************************/
// Set the minimum MagicMirror module version for this module.
requiresVersion: "2.0.0",
// Module config defaults.
defaults: {},
// Timer reference used for showHide animation callbacks.
showHideTimer: null,
// Array to store lockStrings. These strings are used to lock
// visibility when hiding and showing module.
lockStrings: [],
/* init()
* Is called when the module is instantiated.
*/
init: function() {
init: function () {
//Log.log(this.defaults);
},
/* start()
* Is called when the module is started.
*/
start: function() {
start: function () {
Log.info("Starting module: " + this.name);
},
@ -39,7 +46,7 @@ var Module = Class.extend({
*
* return Array<String> - An array with filenames.
*/
getScripts: function() {
getScripts: function () {
return [];
},
@ -48,7 +55,7 @@ var Module = Class.extend({
*
* return Array<String> - An array with filenames.
*/
getStyles: function() {
getStyles: function () {
return [];
},
@ -57,7 +64,7 @@ var Module = Class.extend({
*
* return Map<String, String> - A map with langKeys and filenames.
*/
getTranslations: function() {
getTranslations: function () {
return false;
},
@ -67,7 +74,7 @@ var Module = Class.extend({
*
* return domobject - The dom to display.
*/
getDom: function() {
getDom: function () {
var nameWrapper = document.createElement("div");
var name = document.createTextNode(this.name);
nameWrapper.appendChild(name);
@ -84,15 +91,26 @@ var Module = Class.extend({
return div;
},
/* getHeader()
* This method generates the header string which needs to be displayed if a user has a header configured for this module.
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
*
* return string - The header to display above the header.
*/
getHeader: function () {
return this.data.header;
},
/* notificationReceived(notification, payload, sender)
* This method is called when a notification arrives.
* This method is called by the Magic Mirror core.
*
* argument notification string - The identifier of the noitication.
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
* argument sender Module - The module that sent the notification.
*/
notificationReceived: function(notification, payload, sender) {
notificationReceived: function (notification, payload, sender) {
if (sender) {
Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
} else {
@ -103,24 +121,24 @@ var Module = Class.extend({
/* socketNotificationReceived(notification, payload)
* This method is called when a socket notification arrives.
*
* argument notification string - The identifier of the noitication.
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
socketNotificationReceived: function(notification, payload) {
socketNotificationReceived: function (notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
},
/* suspend()
* This method is called when a module is hidden.
*/
suspend: function() {
suspend: function () {
Log.log(this.name + " is suspended.");
},
/* resume()
* This method is called when a module is shown.
*/
resume: function() {
resume: function () {
Log.log(this.name + " is resumed.");
},
@ -133,7 +151,7 @@ var Module = Class.extend({
*
* argument data obejct - Module data.
*/
setData: function(data) {
setData: function (data) {
this.data = data;
this.name = data.name;
this.identifier = data.identifier;
@ -147,21 +165,21 @@ var Module = Class.extend({
*
* argument config obejct - Module config.
*/
setConfig: function(config) {
this.config = Object.assign(this.defaults, config);
setConfig: function (config) {
this.config = Object.assign({}, this.defaults, config);
},
/* socket()
* Returns a socket object. If it doesn"t exist, it"s created.
* It also registers the notification callback.
*/
socket: function() {
socket: function () {
if (typeof this._socket === "undefined") {
this._socket = this._socket = new MMSocket(this.name);
}
var self = this;
this._socket.setNotificationCallback(function(notification, payload) {
this._socket.setNotificationCallback(function (notification, payload) {
self.socketNotificationReceived(notification, payload);
});
@ -175,7 +193,7 @@ var Module = Class.extend({
*
* return string - File path.
*/
file: function(file) {
file: function (file) {
return this.data.path + "/" + file;
},
@ -184,14 +202,14 @@ var Module = Class.extend({
*
* argument callback function - Function called when done.
*/
loadStyles: function(callback) {
loadStyles: function (callback) {
var self = this;
var styles = this.getStyles();
var loadNextStyle = function() {
var loadNextStyle = function () {
if (styles.length > 0) {
var nextStyle = styles[0];
Loader.loadFile(nextStyle, self, function() {
Loader.loadFile(nextStyle, self, function () {
styles = styles.slice(1);
loadNextStyle();
});
@ -208,14 +226,14 @@ var Module = Class.extend({
*
* argument callback function - Function called when done.
*/
loadScripts: function(callback) {
loadScripts: function (callback) {
var self = this;
var scripts = this.getScripts();
var loadNextScript = function() {
var loadNextScript = function () {
if (scripts.length > 0) {
var nextScript = scripts[0];
Loader.loadFile(nextScript, self, function() {
Loader.loadFile(nextScript, self, function () {
scripts = scripts.slice(1);
loadNextScript();
});
@ -232,14 +250,14 @@ var Module = Class.extend({
*
* argument callback function - Function called when done.
*/
loadTranslations: function(callback) {
loadTranslations: function (callback) {
var self = this;
var translations = this.getTranslations();
var lang = config.language.toLowerCase();
// The variable `first` will contain the first
// defined translation after the following line.
for (var first in translations) {break;}
for (var first in translations) { break; }
if (translations) {
var translationFile = translations[lang] || undefined;
@ -248,7 +266,7 @@ var Module = Class.extend({
// If a translation file is set, load it and then also load the fallback translation file.
// Otherwise only load the fallback translation file.
if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
Translator.load(self, translationFile, false, function() {
Translator.load(self, translationFile, false, function () {
Translator.load(self, translationsFallbackFile, true, callback);
});
} else {
@ -259,13 +277,13 @@ var Module = Class.extend({
}
},
/* translate(key, defaultValue)
* Request the translation for a given key.
*
* argument key string - The key of the string to translage
* argument defaultValue string - The default value if no translation was found. (Optional)
*/
translate: function(key, defaultValue) {
/* translate(key, defaultValue)
* Request the translation for a given key.
*
* argument key string - The key of the string to translage
* argument defaultValue string - The default value if no translation was found. (Optional)
*/
translate: function (key, defaultValue) {
return Translator.translate(this, key) || defaultValue || "";
},
@ -274,27 +292,27 @@ var Module = Class.extend({
*
* argument speed Number - The speed of the animation. (Optional)
*/
updateDom: function(speed) {
updateDom: function (speed) {
MM.updateDom(this, speed);
},
/* sendNotification(notification, payload)
* Send a notification to all modules.
*
* argument notification string - The identifier of the noitication.
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
sendNotification: function(notification, payload) {
sendNotification: function (notification, payload) {
MM.sendNotification(notification, payload, this);
},
/* sendSocketNotification(notification, payload)
* Send a socket notification to the node helper.
*
* argument notification string - The identifier of the noitication.
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
sendSocketNotification: function(notification, payload) {
sendSocketNotification: function (notification, payload) {
this.socket().sendNotification(notification, payload);
},
@ -303,15 +321,22 @@ var Module = Class.extend({
*
* argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/
hide: function(speed, callback) {
callback = callback || function() {};
hide: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () { };
}
callback = callback || function () { };
options = options || {};
var self = this;
MM.hideModule(self, speed, function() {
MM.hideModule(self, speed, function () {
self.suspend();
callback();
});
}, options);
},
/* showModule(module, speed, callback)
@ -319,29 +344,29 @@ var Module = Class.extend({
*
* argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/
show: function(speed, callback) {
show: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () { };
}
callback = callback || function () { };
options = options || {};
this.resume();
MM.showModule(this, speed, callback);
MM.showModule(this, speed, callback, options);
}
});
Module.definitions = {};
Module.create = function(name) {
Module.create = function (name) {
//Define the clone method for later use.
function cloneObject(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
var temp = obj.constructor(); // give temp the original obj"s constructor
for (var key in obj) {
temp[key] = cloneObject(obj[key]);
}
return temp;
// Make sure module definition is available.
if (!Module.definitions[name]) {
return;
}
var moduleDefinition = Module.definitions[name];
@ -354,7 +379,39 @@ Module.create = function(name) {
};
Module.register = function(name, moduleDefinition) {
/* cmpVersions(a,b)
* Compare two symantic version numbers and return the difference.
*
* argument a string - Version number a.
* argument a string - Version number b.
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}
}
return segmentsA.length - segmentsB.length;
}
Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + version);
if (cmpVersions(version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + name + "'");
return;
}
}
Log.log("Module registered: " + name);
Module.definitions[name] = moduleDefinition;
};

View File

@ -10,21 +10,43 @@ var app = require("express")();
var server = require("http").Server(app);
var io = require("socket.io")(server);
var path = require("path");
var ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs");
var helmet = require("helmet");
var Server = function(config, callback) {
console.log("Starting server op port " + config.port + " ... ");
server.listen(config.port);
server.listen(config.port, config.address ? config.address : null);
app.use(function(req, res, next) {
var result = ipfilter(config.ipWhitelist, {mode: "allow", log: false})(req, res, function(err) {
if (err === undefined) {
return next();
}
console.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
});
});
app.use(helmet());
app.use("/js", express.static(__dirname));
app.use("/config", express.static(path.resolve(__dirname + "/../config")));
app.use("/css", express.static(path.resolve(__dirname + "/../css")));
app.use("/fonts", express.static(path.resolve(__dirname + "/../fonts")));
app.use("/modules", express.static(path.resolve(__dirname + "/../modules")));
app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor")));
app.use("/translations", express.static(path.resolve(__dirname + "/../translations")));
app.use("/config", express.static(path.resolve(global.root_path + "/config")));
app.use("/css", express.static(path.resolve(global.root_path + "/css")));
app.use("/fonts", express.static(path.resolve(global.root_path + "/fonts")));
app.use("/modules", express.static(path.resolve(global.root_path + "/modules")));
app.use("/vendor", express.static(path.resolve(global.root_path + "/vendor")));
app.use("/translations", express.static(path.resolve(global.root_path + "/translations")));
app.get("/version", function(req,res) {
res.send(global.version);
});
app.get("/", function(req, res) {
res.sendFile(path.resolve(__dirname + "/../index.html"));
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
html = html.replace("#VERSION#", global.version);
res.send(html);
});
if (typeof callback === "function") {

13
jsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=759670
// for the documentation about the jsconfig.json format
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"exclude": [
"modules",
"node_modules"
]
}

View File

@ -6,11 +6,11 @@ This document describes the way to develop your own MagicMirror² modules.
All modules are loaded in the `modules` folder. The default modules are grouped together in the `modules/default` folder. Your module should be placed in a subfolder of `modules`. Note that any file or folder your create in the `modules` folder will be ignored by git, allowing you to upgrade the MagicMirror² without the loss of your files.
A module can be placed in one single folder. Or multiple modules can be grouped in a subfoler. Note that name of the module must be unique. Even when a module with a similar name is placed in a different folder, they can't be loaded at the same time.
A module can be placed in one single folder. Or multiple modules can be grouped in a subfolder. Note that name of the module must be unique. Even when a module with a similar name is placed in a different folder, they can't be loaded at the same time.
### Files
- **modulename/modulename.js** - This is your core module script.
- **modulename/node_helper.js** - This is an optional helper that whill be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
- **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles.
@ -78,6 +78,19 @@ The data object contains additional metadata about the module instance:
####`defaults: {}`
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
####'requiresVersion:'
*Introduced in version: 2.1.0.*
A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module. Make sure to also set this value in the Node helper.
**Note:** Since this check is introduced in version 2.1.0, this check will not be run in older versions. Keep this in mind if you get issue reports on your module.
Example:
````javascript
requiresVersion: "2.1.0",
````
### Subclassable module methods
####`init()`
@ -104,7 +117,7 @@ The getScripts method is called to request any additional scripts that need to b
getScripts: function() {
return [
'script.js', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
'moment.js', // this file is available in the vendor folder, so it doesn't need to be avialable in the module folder.
'moment.js', // this file is available in the vendor folder, so it doesn't need to be available in the module folder.
this.file('anotherfile.js'), // this file will be loaded straight from the module folder.
'https://code.jquery.com/jquery-2.2.3.min.js', // this file will be loaded from the jquery servers.
]
@ -154,7 +167,7 @@ getTranslations: function() {
####`getDom()`
**Should return:** Dom Object
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefor return a dom object.
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefore return a dom object.
**Example:**
````javascript
@ -166,6 +179,23 @@ getDom: function() {
````
####`getHeader()`
**Should return:** String
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getHeader method to retrieve the module's header. This method should therefor return a string. If this method is not subclassed, this function will return the user's configured header.
If you want to use the original user's configured header, reference `this.data.header`.
**NOTE:** If the user did not configure a default header, no header will be displayed and thus this method will not be called.
**Example:**
````javascript
getHeader: function() {
return this.data.header + ' Foo Bar';
}
````
####`notificationReceived(notification, payload, sender)`
That MagicMirror core has the ability to send notifications to modules. Or even better: the modules have the possibility to send notifications to other modules. When this module is called, it has 3 arguments:
@ -185,7 +215,7 @@ notificationReceived: function(notification, payload, sender) {
}
````
**Note:** the system sends two notifiations when starting up. These notifications could come in handy!
**Note:** the system sends two notifications when starting up. These notifications could come in handy!
- `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules.
@ -198,8 +228,8 @@ When using a node_helper, the node helper can send your module notifications. Wh
- `notification` - String - The notification identifier.
- `payload` - AnyType - The payload of a notification.
**Note 1:** When a node helper send a notification, all modules of that module type receive the same notifications. <br>
**Note 2:** The socket connection is established as soon as the module sends it's first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications. <br>
**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Example:**
````javascript
@ -217,7 +247,7 @@ When a module will be shown after it was previously hidden (using the `module.sh
### Module instance methods
Each module instance has some handy methods which can be helpfull building your module.
Each module instance has some handy methods which can be helpful building your module.
####`this.file(filename)`
@ -229,7 +259,7 @@ If you want to create a path to a file in your module folder, use the `file()` m
####`this.updateDom(speed)`
***speed* Number** - Optional. Animation speed in milliseconds.<br>
Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update it's dom object. If you define the speed, the content update will be animated, but only if the content will realy change.
Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update its dom object. If you define the speed, the content update will be animated, but only if the content will really change.
As an example: the clock modules calls this method every second:
@ -248,7 +278,7 @@ start: function() {
***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to all other modules, use the `sendNotification(notification, payload)`. All other modules will receive the message via the [notificationReceived](#notificationreceivednotification-payload-sender) method. In that case, the sender is automaticly set to the instance calling the sendNotification method.
If you want to send a notification to all other modules, use the `sendNotification(notification, payload)`. All other modules will receive the message via the [notificationReceived](#notificationreceivednotification-payload-sender) method. In that case, the sender is automatically set to the instance calling the sendNotification method.
**Example:**
````javascript
@ -259,33 +289,103 @@ this.sendNotification('MYMODULE_READY_FOR_ACTION', {foo:bar});
***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to the node_helper, use the `sendSocketNotification(notification, payload)`. Only the node_helper of this module will recieve the socket notification.
If you want to send a notification to the node_helper, use the `sendSocketNotification(notification, payload)`. Only the node_helper of this module will receive the socket notification.
**Example:**
````javascript
this.sendSocketNotification('SET_CONFIG', this.config);
````
####`this.hide(speed, callback)`
***speed* Number** - Optional, The speed of the hide animation in milliseconds.
####`this.hide(speed, callback, options)`
***speed* Number** - Optional (Required when setting callback or options), The speed of the hide animation in milliseconds.
***callback* Function** - Optional, The callback after the hide animation is finished.
***options* Function** - Optional, Object with additional options for the hide action (see below). (*Introduced in version: 2.1.0.*)
To hide a module, you can call the `hide(speed, callback)` method. You can call the hide method on the module instance itself using `this.hide()`, but of course you can also hide another module using `anOtherModule.hide()`.
Possible configurable options:
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
To hide a module, you can call the `hide(speed, callback)` method. You can call the hide method on the module instance itselve using `this.hide()`, but of course you can also hide an other module using `anOtherModule.hide()`.
**Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
####`this.show(speed, callback)`
***speed* Number** - Optional, The speed of the show animation in milliseconds.
***callback* Function** - Optional, The callback after the show animation is finished.
To show a module, you can call the `show(speed, callback)` method. You can call the show method on the module instance itselve using `this.show()`, but of course you can also show an other module using `anOtherModule.show()`.
####`this.show(speed, callback, options)`
***speed* Number** - Optional (Required when setting callback or options), The speed of the show animation in milliseconds.
***callback* Function** - Optional, The callback after the show animation is finished.
***options* Function** - Optional, Object with additional options for the show action (see below). (*Introduced in version: 2.1.0.*)
To show a module, you can call the `show(speed, callback)` method. You can call the show method on the module instance itself using `this.show()`, but of course you can also show another module using `anOtherModule.show()`.
Possible configurable options:
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. See *visibility locking* below.
- `force` - Boolean - When setting the force tag to `true`, the locking mechanism will be overwritten. Use this option with caution. It's considered best practice to let the usage of the force option be use- configurable. See *visibility locking* below.
**Note 1:** If the show animation is canceled, for instance because the hide method is called before the show animation was finished, the callback will not be called.<br>
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.<br>
**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).
####Visibility locking
(*Introduced in version: 2.1.0.*)
Visiblity locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
**Module B asks module A to hide:**
````javascript
moduleA.hide(0, {lockString: "module_b_identifier"});
````
Module A is now hidden, and has an lock array with the following strings:
````javascript
moduleA.lockStrings == ["module_b_identifier"]
moduleA.hidden == true
````
**Module C asks module A to hide:**
````javascript
moduleA.hide(0, {lockString: "module_c_identifier"});
````
Module A is now hidden, and has an lock array with the following strings:
````javascript
moduleA.lockStrings == ["module_b_identifier", "module_c_identifier"]
moduleA.hidden == true
````
**Module B asks module A to show:**
````javascript
moduleA.show(0, {lockString: "module_b_identifier"});
````
The lockString will be removed from moduleAs locks array, but since there still is an other lock string available, the module remains hidden:
````javascript
moduleA.lockStrings == ["module_c_identifier"]
moduleA.hidden == true
````
**Module C asks module A to show:**
````javascript
moduleA.show(0, {lockString: "module_c_identifier"});
````
The lockString will be removed from moduleAs locks array, and since this will result in an empty lock array, the module will be visible:
````javascript
moduleA.lockStrings == []
moduleA.hidden == false
````
**Note:** The locking mechanism can be overwritten by using the force tag:
````javascript
moduleA.show(0, {force: true});
````
This will reset the lockstring array, and will show the module.
````javascript
moduleA.lockStrings == []
moduleA.hidden == false
````
Use this `force` method with caution. See `show()` method for more information.
####`this.translate(identifier)`
***identifier* String** - Identifier of the string that should be translated.
@ -328,7 +428,7 @@ var NodeHelper = require("node_helper");
module.exports = NodeHelper.create({});
````
Of course, the above helper would not do anything usefull. So with the information above, you should be able to make it a bit more sophisticated.
Of course, the above helper would not do anything useful. So with the information above, you should be able to make it a bit more sophisticated.
### Available module instance properties
@ -356,7 +456,7 @@ start: function() {
}
````
**Note: ** By default, a public path to your module's public folder will be created:
**Note:** By default, a public path to your module's public folder will be created:
````javascript
this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
````
@ -366,13 +466,26 @@ this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple.
####'requiresVersion:'
*Introduced in version: 2.1.0.*
A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module.
**Note:** Since this check is introduced in version 2.1.0, this check will not be run in older versions. Keep this in mind if you get issue reports on your module.
Example:
````javascript
requiresVersion: "2.1.0",
````
### Subclassable module methods
####`init()`
This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method.
####`start()`
This method is called when all node helper are loaded an the system is ready to boot up. The start method is a perfect place to define any additional module properties:
This method is called when all node helpers are loaded and the system is ready to boot up. The start method is a perfect place to define any additional module properties:
**Example:**
````javascript
@ -383,12 +496,12 @@ start: function() {
````
####`socketNotificationReceived: function(notification, payload)`
With this method, your node helper can receive notifications form your modules. When this method is called, it has 2 arguments:
With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:
- `notification` - String - The notification identifier.
- `payload` - AnyType - The payload of a notification.
**Note:** The socket connection is established as soon as the module sends it's first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Note:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Example:**
````javascript
@ -399,15 +512,15 @@ socketNotificationReceived: function(notification, payload) {
### Module instance methods
Each node helper has some handy methods which can be helpfull building your module.
Each node helper has some handy methods which can be helpful building your module.
####`this.sendSocketNotification(notification, payload)`
***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to all your modules, use the `sendSocketNotification(notification, payload)`. Only the module of your module type will recieve the socket notification.
If you want to send a notification to all your modules, use the `sendSocketNotification(notification, payload)`. Only the module of your module type will receive the socket notification.
**Note:** Since all instances of you module will receive the notifications, it's your task to make sure the right module responds to your messages.
**Note:** Since all instances of your module will receive the notifications, it's your task to make sure the right module responds to your messages.
**Example:**
````javascript
@ -430,10 +543,10 @@ To make a selection of all currently loaded module instances, run the `MM.getMod
#####`.withClass(classnames)`
***classnames* String or Array** - The class names on which you want to filer.
***classnames* String or Array** - The class names on which you want to filter.
**Returns Array** - An array with module instances.<br>
If you want to make a selection based on one ore more class names, use the withClass method on a result of the `MM.getModules()` method. The argument of the `withClass(classname)` method can be an array, or space separated string.
If you want to make a selection based on one or more class names, use the withClass method on a result of the `MM.getModules()` method. The argument of the `withClass(classname)` method can be an array, or space separated string.
**Examples:**
````javascript
@ -459,7 +572,7 @@ var modules = MM.getModules().exceptWithClass(['classname1','classname2']);
***module* Module Object** - The reference to a module you want to remove from the results.
**Returns Array** - An array with module instances.<br>
If you to remove a specific module instance from a selection based on a classname, use the exceptWithClass method on a result of the `MM.getModules()` method. This can be helpfull if you want to select all module instances except the instance of your module.
If you to remove a specific module instance from a selection based on a classname, use the exceptWithClass method on a result of the `MM.getModules()` method. This can be helpful if you want to select all module instances except the instance of your module.
**Examples:**
````javascript

View File

@ -45,9 +45,9 @@ The following properties can be configured:
</tr>
<tr>
<td><code>display_time</code></td>
<td>Time a notification is displayed in seconds.<br>
<br><b>Possible values:</b> <code>float</code> <code>int</code>
<br><b>Default value:</b> <code>3.5</code>
<td>Time a notification is displayed in milliseconds.<br>
<br><b>Possible values:</b> <code>int</code>
<br><b>Default value:</b> <code>3500</code>
</td>
</tr>
<tr>
@ -62,7 +62,7 @@ The following properties can be configured:
<td><code>welcome_message</code></td>
<td>Message shown at startup.<br>
<br><b>Possible values:</b> <code>string</code> <code>false</code>
<br><b>Default value:</b> <code>Welcome, start was successfull!</code>
<br><b>Default value:</b> <code>false</code> (no message at startup)
</td>
</tr>
</tbody>
@ -162,4 +162,4 @@ self.sendNotification("SHOW_ALERT", {});
## Open Source Licenses
###[NotificationStyles](https://github.com/codrops/NotificationStyles)
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.

View File

@ -35,7 +35,6 @@ Module.register("alert",{
},
show_notification: function(message) {
if (this.config.effect == "slide") {this.config.effect = this.config.effect + "-" + this.config.position;}
msg = "";
if (message.title) {
msg += "<span class='thin' style='line-height: 35px; font-size:24px' color='#4A4A4A'>" + message.title + "</span>";
@ -46,7 +45,7 @@ Module.register("alert",{
}
msg += "<span class='light' style='font-size:28px;line-height: 30px;'>" + message.message + "</span>";
}
new NotificationFx({
message: msg,
layout: "growl",

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror Notifikation",
"welcome": "Velkommen, modulet er succesfuldt startet!"
}

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror Notificaciones",
"welcome": "Bienvenido, ¡se iniciado correctamente!"
}

View File

@ -106,10 +106,16 @@ The following properties can be configured:
<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>
<br><b>Default value:</b>
<code>
{
"De verjaardag van ": "",
"'s birthday": ""
}
</code>
</td>
</tr>
<tr>
@ -119,6 +125,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>dateFormat</code></td>
<td>Format to use for the date of events (when using absolute dates)<br>
<br><b>Possible values:</b> See <a href="http://momentjs.com/docs/#/parsing/string-format/">Moment.js formats</a>
<br><b>Default value:</b> <code>MMM Do</code> (e.g. Jan 18th)
</td>
</tr>
<tr>
<td><code>timeFormat</code></td>
<td>Display event times as absolute dates, or relative time<br>
@ -126,12 +139,33 @@ The following properties can be configured:
<br><b>Default value:</b> <code>relative</code>
</td>
</tr>
<tr>
<td><code>getRelative</code></td>
<td>How much time (in hours) should be left until calendar events start getting relative?<br>
<br><b>Possible values:</b> <code>0</code> (events stay absolute) - <code>48</code> (48 hours before the event starts)
<br><b>Default value:</b> <code>6</code>
</td>
</tr>
<tr>
<td><code>urgency</code></td>
<td>When using a timeFormat of <code>absolute</code>, the <code>urgency</code> setting allows you to display events within a specific time frame as <code>relative</code>
This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates<br>
<br><b>Possible values:</b> a positive integer representing the number of days for which you want a relative date, for example <code>7</code> (for 7 days)<br>
<br><b>Default value:</b> <code>0</code> (disabled)
<br><b>Default value:</b> <code>7</code>
</td>
</tr>
<tr>
<td><code>broadcastEvents</code></td>
<td>If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: <code>CALENDAR_EVENTS</code>. The event objects are stored in an array and contain the following fields: <code>title</code>, <code>startDate</code>, <code>endDate</code>, <code>fullDayEvent</code>, <code>location</code> and <code>geo</code>.<br>
<br><b>Possible values:</b> <code>true</code>, <code>false</code> <br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>hidePrivate</code></td>
<td>Hides private calendar events.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
</tbody>
@ -171,24 +205,24 @@ config: {
</td>
</tr>
<tr>
<td><code> symbol </code></td>
<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 Awesome</a> website.
</td>
</tr>
<tr>
<td><code> repeatingCountTitle </code></td>
<td><code>repeatingCountTitle</code></td>
<td>The count title for yearly repating events in this calendar. <br>
<br><b>Example:</b> <br>
<code>'Birthday'</code>
</td>
</tr>
<tr>
<td><code> user </code></td>
<td><code>user</code></td>
<td>The username for HTTP Basic authentication.</td>
</tr>
<tr>
<td><code> pass </code></td>
<td><code>pass</code></td>
<td>The password for HTTP Basic authentication.</td>
</tr>
</tbody>

View File

@ -7,7 +7,7 @@
* MIT Licensed.
*/
Module.register("calendar",{
Module.register("calendar", {
// Define module defaults
defaults: {
@ -16,14 +16,17 @@ Module.register("calendar",{
displaySymbol: true,
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
displayRepeatingCountTitle: false,
defaultRepeatingCountTitle: '',
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000,
fade: true,
urgency: 7,
timeFormat: "relative",
dateFormat: "MMM Do",
getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
calendars: [
{
symbol: "calendar",
@ -34,20 +37,21 @@ Module.register("calendar",{
"De verjaardag van ": "",
"'s birthday": ""
},
broadcastEvents: true
},
// Define required scripts.
getStyles: function() {
getStyles: function () {
return ["calendar.css", "font-awesome.css"];
},
// Define required scripts.
getScripts: function() {
getScripts: function () {
return ["moment.js"];
},
// Define required translations.
getTranslations: function() {
getTranslations: function () {
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build your own module including translations, check out the documentation.
@ -55,7 +59,7 @@ Module.register("calendar",{
},
// Override start method.
start: function() {
start: function () {
Log.log("Starting module: " + this.name);
// Set locale.
@ -72,11 +76,15 @@ Module.register("calendar",{
},
// Override socket notification handler.
socketNotificationReceived: function(notification, payload) {
socketNotificationReceived: function (notification, payload) {
if (notification === "CALENDAR_EVENTS") {
if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events;
this.loaded = true;
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
}
} else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
@ -90,7 +98,7 @@ Module.register("calendar",{
},
// Override dom generator.
getDom: function() {
getDom: function () {
var events = this.createEventList();
var wrapper = document.createElement("table");
@ -109,27 +117,27 @@ Module.register("calendar",{
eventWrapper.className = "normal";
if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td");
var symbolWrapper = document.createElement("td");
symbolWrapper.className = "symbol";
var symbol = document.createElement("span");
var symbol = document.createElement("span");
symbol.className = "fa fa-" + this.symbolForUrl(event.url);
symbolWrapper.appendChild(symbol);
eventWrapper.appendChild(symbolWrapper);
}
var titleWrapper = document.createElement("td"),
repeatingCountTitle = '';
repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle) {
repeatingCountTitle = this.countTitleForUrl(event.url);
if(repeatingCountTitle !== '') {
var thisYear = new Date().getFullYear(),
if (repeatingCountTitle !== "") {
var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ', '+ yearDiff + '. ' + repeatingCountTitle;
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
}
}
@ -137,28 +145,24 @@ Module.register("calendar",{
titleWrapper.className = "title bright";
eventWrapper.appendChild(titleWrapper);
var timeWrapper = document.createElement("td");
var timeWrapper = document.createElement("td");
//console.log(event.today);
var now = new Date();
// Define second, minute, hour, and day variables
var one_second = 1000; // 1,000 milliseconds
var one_minute = one_second * 60;
var one_hour = one_minute * 60;
var one_day = one_hour * 24;
var oneSecond = 1000; // 1,000 milliseconds
var oneMinute = oneSecond * 60;
var oneHour = oneMinute * 60;
var oneDay = oneHour * 24;
if (event.fullDayEvent) {
if (event.today) {
timeWrapper.innerHTML = this.translate("TODAY");
} else if (event.startDate - now < one_day && event.startDate - now > 0) {
timeWrapper.innerHTML = this.translate("TOMORROW");
} else if (event.startDate - now < 2*one_day && event.startDate - now > 0) {
/*Provide ability to show "the day after tomorrow" instead of "in a day"
*if "DAYAFTERTOMORROW" is configured in a language's translation .json file,
*,which can be found in MagicMirror/translations/
*/
if (this.translate('DAYAFTERTOMORROW') !== 'DAYAFTERTOMORROW') {
timeWrapper.innerHTML = this.translate("DAYAFTERTOMORROW");
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
} else {
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
} else {
/* Check to see if the user displays absolute or relative dates with their events
@ -169,26 +173,26 @@ Module.register("calendar",{
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/
if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * one_day))) {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do");
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
}
} else {
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
}
} else {
if (event.startDate >= new Date()) {
if (event.startDate - now < 2 * one_day) {
if (event.startDate - now < 2 * oneDay) {
// This event is within the next 48 hours (2 days)
if (event.startDate - now < 6 * one_hour) {
if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
// Otherwise just say 'Today/Tomorrow at such-n-such time'
timeWrapper.innerHTML = moment(event.startDate, "x").calendar();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
}
} else {
/* Check to see if the user displays absolute or relative dates with their events
@ -199,18 +203,18 @@ Module.register("calendar",{
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/
if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * one_day))) {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do");
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
}
} else {
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow();
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
}
} else {
timeWrapper.innerHTML = this.translate("RUNNING") + ' ' + moment(event.endDate,"x").fromNow(true);
timeWrapper.innerHTML = this.capFirst(this.translate("RUNNING")) + " " + moment(event.endDate, "x").fromNow(true);
}
}
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
@ -244,7 +248,7 @@ Module.register("calendar",{
*
* return bool - Has calendar url
*/
hasCalendarURL: function(url) {
hasCalendarURL: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url) {
@ -260,20 +264,26 @@ Module.register("calendar",{
*
* return array - Array with events.
*/
createEventList: function() {
createEventList: function () {
var events = [];
var today = moment().startOf("day");
for (var c in this.calendarData) {
var calendar = this.calendarData[c];
for (var e in calendar) {
var event = calendar[e];
if(this.config.hidePrivate) {
if(event.class === "PRIVATE") {
// do not add the current event, skip it
continue;
}
}
event.url = c;
event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000);
events.push(event);
}
}
events.sort(function(a, b) {
events.sort(function (a, b) {
return a.startDate - b.startDate;
});
@ -285,7 +295,7 @@ Module.register("calendar",{
*
* argument url sting - Url to add.
*/
addCalendar: function(url, user, pass) {
addCalendar: function (url, user, pass) {
this.sendSocketNotification("ADD_CALENDAR", {
url: url,
maximumEntries: this.config.maximumEntries,
@ -303,10 +313,10 @@ Module.register("calendar",{
*
* return string - The Symbol
*/
symbolForUrl: function(url) {
symbolForUrl: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.symbol === "string") {
if (calendar.url === url && typeof calendar.symbol === "string") {
return calendar.symbol;
}
}
@ -320,10 +330,10 @@ Module.register("calendar",{
*
* return string - The Symbol
*/
countTitleForUrl: function(url) {
countTitleForUrl: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") {
if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") {
return calendar.repeatingCountTitle;
}
}
@ -340,14 +350,23 @@ Module.register("calendar",{
*
* return string - The shortened string.
*/
shorten: function(string, maxLength) {
shorten: function (string, maxLength) {
if (string.length > maxLength) {
return string.slice(0,maxLength) + "&hellip;";
return string.slice(0, maxLength) + "&hellip;";
}
return string;
},
/* capFirst(string)
* Capitalize the first letter of a string
* Eeturn capitalized string
*/
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/* titleTransform(title)
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
@ -357,7 +376,7 @@ Module.register("calendar",{
*
* return string - The transformed title.
*/
titleTransform: function(title) {
titleTransform: function (title) {
for (var needle in this.config.titleReplace) {
var replacement = this.config.titleReplace[needle];
title = title.replace(needle, replacement);
@ -365,5 +384,28 @@ Module.register("calendar",{
title = this.shorten(title, this.config.maxTitleLength);
return title;
},
/* broadcastEvents()
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
var eventList = [];
for (url in this.calendarData) {
var calendar = this.calendarData[url];
for (e in calendar) {
var event = cloneObject(calendar[e]);
delete event.url;
eventList.push(event);
}
}
eventList.sort(function(a,b) {
return a.startDate - b.startDate;
});
this.sendNotification("CALENDAR_EVENTS", eventList);
}
});

View File

@ -25,9 +25,10 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
clearTimeout(reloadTimer);
reloadTimer = null;
nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
var opts = {
headers: {
'User-Agent': 'Mozilla/5.0 (Node.js 6.0.0) MagicMirror/v2 (https://github.com/MichMich/MagicMirror/)'
"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
}
};
@ -77,10 +78,11 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
if (!isFacebookBirthday) {
endDate = startDate;
} else {
endDate = moment(startDate).add(1, 'days');
endDate = moment(startDate).add(1, "days");
}
}
// calculate the duration f the event for use with recurring events.
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
@ -95,20 +97,28 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
title = event.description;
}
var location = event.location || false;
var geo = event.geo || false;
var description = event.description || false;
if (typeof event.rrule != "undefined" && !isFacebookBirthday) {
var rule = event.rrule;
var dates = rule.between(today, future, true, limitFunction);
for (var d in dates) {
startDate = moment(new Date(dates[d]));
endDate = moment(parseInt(startDate.format("x")) + duration, 'x');
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (endDate.format("x") > now) {
newEvents.push({
title: title,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
fullDayEvent: isFullDayEvent(event),
firstYear: event.start.getFullYear()
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
@ -132,14 +142,19 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
continue;
}
// Every thing is good. Add it to the list.
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
fullDayEvent: fullDayEvent
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
}
}
}
@ -186,7 +201,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
if (end - start === 24 * 60 * 60 * 1000 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
return true;
}
return false;

View File

@ -1,5 +1,5 @@
/* CalendarFetcher Tester
* use this script with `node debug.js` to test the fetcher without the need
* use this script with `node debug.js` to test the fetcher without the need
* of starting the MagicMirror core. Adjust the values below to your desire.
*
* By Michael Teeuw http://michaelteeuw.nl
@ -8,18 +8,18 @@
var CalendarFetcher = require("./calendarfetcher.js");
var url = 'https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics';
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics";
var fetchInterval = 60 * 60 * 1000;
var maximumEntries = 10;
var maximumNumberOfDays = 365;
console.log('Create fetcher ...');
console.log("Create fetcher ...");
fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays);
fetcher.onReceive(function(fetcher) {
console.log(fetcher.events());
console.log('------------------------------------------------------------');
console.log(fetcher.events());
console.log("------------------------------------------------------------");
});
fetcher.onError(function(fetcher, error) {
@ -29,5 +29,4 @@ fetcher.onError(function(fetcher, error) {
fetcher.startFetch();
console.log('Create fetcher done! ');
console.log("Create fetcher done! ");

View File

@ -115,5 +115,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>top</code>
</td>
</tr>
<tr>
<td><code>timezone</code></td>
<td>Specific a timezone to show clock.<br>
<br><b>Possible examples values:</b> <code>America/New_York</code>, <code>America/Santiago</code>, <code>Etc/GMT+10</code>
<br><b>Default value:</b> <code>none</code>
</td>
</tr>
</tbody>
</table>
</table>

View File

@ -8,7 +8,7 @@
Module.register("clock",{
// Module config defaults.
defaults: {
displayType: 'digital', // options: digital, analog, both
displayType: "digital", // options: digital, analog, both
timeFormat: config.timeFormat,
displaySeconds: true,
@ -18,15 +18,16 @@ Module.register("clock",{
showDate: true,
/* specific to the analog clock */
analogSize: '200px',
analogFace: 'simple', // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: 'bottom', // options: 'top', 'bottom', 'left', 'right'
analogShowDate: 'top', // options: false, 'top', or 'bottom'
secondsColor: '#888888',
analogSize: "200px",
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // options: false, 'top', or 'bottom'
secondsColor: "#888888",
timezone: null,
},
// Define required scripts.
getScripts: function() {
return ["moment.js"];
return ["moment.js", "moment-timezone.js"];
},
// Define styles.
getStyles: function() {
@ -69,10 +70,14 @@ Module.register("clock",{
// So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181
var timeString;
var now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
if (this.config.clockBold === true) {
timeString = moment().format("HH[<span class=\"bold\">]mm[</span>]");
timeString = now.format("HH[<span class=\"bold\">]mm[</span>]");
} else {
timeString = moment().format("HH:mm");
timeString = now.format("HH:mm");
}
if (this.config.timeFormat !== 24) {
@ -80,27 +85,21 @@ Module.register("clock",{
// var hours = now.getHours() % 12 || 12;
if (this.config.clockBold === true) {
//timeString = hours + moment().format("[<span class=\"bold\">]mm[</span>]");
timeString = moment().format("h[<span class=\"bold\">]mm[</span>]");
timeString = now.format("h[<span class=\"bold\">]mm[</span>]");
} else {
//timeString = hours + moment().format(":mm");
timeString = moment().format("h:mm");
timeString = now.format("h:mm");
}
}
if(this.config.showDate){
dateWrapper.innerHTML = moment().format("dddd, LL");
dateWrapper.innerHTML = now.format("dddd, LL");
}
timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = moment().format("ss");
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = moment().format("A");
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = moment().format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
@ -113,12 +112,15 @@ Module.register("clock",{
* Create wrappers for ANALOG clock, only if specified in config
*/
if (this.config.displayType !== 'digital') {
if (this.config.displayType !== "digital") {
// If it isn't 'digital', then an 'analog' clock was also requested
// Calculate the degree offset for each hand of the clock
var now = moment(),
second = now.seconds() * 6,
var now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
var second = now.seconds() * 6,
minute = now.minute() * 6 + second / 60,
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
@ -129,10 +131,10 @@ Module.register("clock",{
clockCircle.style.width = this.config.analogSize;
clockCircle.style.height = this.config.analogSize;
if (this.config.analogFace != '' && this.config.analogFace != 'simple' && this.config.analogFace != 'none') {
if (this.config.analogFace != "" && this.config.analogFace != "simple" && this.config.analogFace != "none") {
clockCircle.style.background = "url("+ this.data.path + "faces/" + this.config.analogFace + ".svg)";
clockCircle.style.backgroundSize = "100%";
} else if (this.config.analogFace != 'none') {
} else if (this.config.analogFace != "none") {
clockCircle.style.border = "2px solid white";
}
var clockFace = document.createElement("div");
@ -166,18 +168,18 @@ Module.register("clock",{
* Combine wrappers, check for .displayType
*/
if (this.config.displayType === 'digital') {
if (this.config.displayType === "digital") {
// Display only a digital clock
wrapper.appendChild(dateWrapper);
wrapper.appendChild(timeWrapper);
} else if (this.config.displayType === 'analog') {
} else if (this.config.displayType === "analog") {
// Display only an analog clock
dateWrapper.style.textAlign = "center";
dateWrapper.style.paddingBottom = "15px";
if (this.config.analogShowDate === 'top') {
if (this.config.analogShowDate === "top") {
wrapper.appendChild(dateWrapper);
wrapper.appendChild(clockCircle);
} else if (this.config.analogShowDate === 'bottom') {
} else if (this.config.analogShowDate === "bottom") {
wrapper.appendChild(clockCircle);
wrapper.appendChild(dateWrapper);
} else {
@ -197,11 +199,11 @@ Module.register("clock",{
digitalWrapper.appendChild(dateWrapper);
digitalWrapper.appendChild(timeWrapper);
if (placement === 'left' || placement === 'right') {
if (placement === "left" || placement === "right") {
digitalWrapper.style.display = "inline-block";
digitalWrapper.style.verticalAlign = "top";
analogWrapper.style.display = "inline-block";
if (placement === 'left') {
if (placement === "left") {
analogWrapper.style.padding = "0 20px 0 0";
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
@ -212,13 +214,13 @@ Module.register("clock",{
}
} else {
digitalWrapper.style.textAlign = "center";
if (placement === 'top') {
if (placement === "top") {
analogWrapper.style.padding = "0 0 20px 0";
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
} else {
analogWrapper.style.padding = "20px 0 0 0";
wrapper.appendChild(digitalWrapper);
wrapper.appendChild(digitalWrapper);
wrapper.appendChild(analogWrapper);
}
}
@ -227,4 +229,4 @@ Module.register("clock",{
// Return the wrapper to the dom.
return wrapper;
}
});
});

View File

@ -56,6 +56,15 @@ The following properties can be configured:
<br><b>Default value:</b> See <i>compliment configuration</i> below.
</td>
</tr>
<tr>
<td><code>remoteFile</code></td>
<td>External file from which to load the compliments<br>
<br><b>Possible values:</b>Path to a JSON file containing compliments, configured
as per the value of the <i>compliments configuration</i> (see below). An object with three arrays:
morning, afternoon and evening. - <code>compliments.json</code>
<br><b>Default value:</b> <code>null</code> (Do not load from file)
</td>
</tr>
</tbody>
</table>
@ -63,6 +72,44 @@ The following properties can be configured:
The `compliments` property contains an object with three arrays: <code>morning</code>, <code>afternoon</code> and<code>evening</code>. Based on the time of the day, the compliments will be picked out of one of these arrays. The arrays contain one or multiple compliments.
If use the currentweather is possible use a actual weather for set compliments. The availables properties are:
* <code>day_sunny</code>
* <code>day_cloudy</code>
* <code>cloudy</code>
* <code>cloudy_windy</code>
* <code>showers</code>
* <code>rain</code>
* <code>thunderstorm</code>
* <code>snow</code>
* <code>fog</code>
* <code>night_clear</code>
* <code>night_cloudy</code>
* <code>night_showers</code>
* <code>night_rain</code>
* <code>night_thunderstorm</code>
* <code>night_snow</code>
* <code>night_alt_cloudy_windy</code>
#### Example use with currentweather module
````javascript
config: {
compliments: {
day_sunny: [
'Today is a sunny day',
'It\'s a beautiful day'
],
snow: [
'Snowball battle!'
],
rain: [
'Don\'t forget your umbrella'
]
}
}
````
#### Default value:
````javascript
config: {
@ -85,3 +132,32 @@ config: {
}
}
````
### External Compliment File
You may specify an external file that contains the three compliment arrays. This is particularly useful if you have a
large number of compliments and do not wish to crowd your `config.js` file with a large array of compliments.
Adding the `remoteFile` variable will override an array you specify in the configuration file.
This file must be straight JSON. Note that the array names need quotes
around them ("morning", "afternoon", "evening", "snow", "rain", etc.).
#### Example compliments.json file:
````json
{
"morning" : [
"Good morning, sunshine!",
"Who needs coffee when you have your smile?",
"Go get 'em, Tiger!"
],
"afternoon" : [
"Hitting your stride!",
"You are making a difference!",
"You're more fun than bubble wrap!"
],
"evening" : [
"You made someone smile today, I know it.",
"You are making a difference.",
"The day was better for your efforts."
]
}
````

View File

@ -29,9 +29,13 @@ Module.register("compliments",{
]
},
updateInterval: 30000,
remoteFile: null,
fadeSpeed: 4000
},
// Set currentweather from module
currentWeatherType: "",
// Define required scripts.
getScripts: function() {
return ["moment.js"];
@ -43,6 +47,12 @@ Module.register("compliments",{
this.lastComplimentIndex = -1;
if (this.config.remoteFile != null) {
this.complimentFile((response) => {
this.config.compliments = JSON.parse(response);
});
}
// Schedule update timer.
var self = this;
setInterval(function() {
@ -84,14 +94,36 @@ Module.register("compliments",{
*/
complimentArray: function() {
var hour = moment().hour();
var compliments = null;
if (hour >= 3 && hour < 12) {
return this.config.compliments.morning;
compliments = this.config.compliments.morning;
} else if (hour >= 12 && hour < 17) {
return this.config.compliments.afternoon;
compliments = this.config.compliments.afternoon;
} else {
return this.config.compliments.evening;
compliments = this.config.compliments.evening;
}
if ( this.currentWeatherType in this.config.compliments) {
compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
}
return compliments;
},
/* complimentFile(callback)
* Retrieve a file from the local filesystem
*/
complimentFile: function(callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open("GET", this.file(this.config.remoteFile), true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
},
/* complimentArray()
@ -112,10 +144,44 @@ Module.register("compliments",{
var compliment = document.createTextNode(complimentText);
var wrapper = document.createElement("div");
wrapper.className = "thin xlarge bright";
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright";
wrapper.appendChild(compliment);
return wrapper;
}
},
// From data currentweather set weather type
setCurrentWeatherType: function(data) {
var weatherIconTable = {
"01d": "day_sunny",
"02d": "day_cloudy",
"03d": "cloudy",
"04d": "cloudy_windy",
"09d": "showers",
"10d": "rain",
"11d": "thunderstorm",
"13d": "snow",
"50d": "fog",
"01n": "night_clear",
"02n": "night_cloudy",
"03n": "night_cloudy",
"04n": "night_cloudy",
"09n": "night_showers",
"10n": "night_rain",
"11n": "night_thunderstorm",
"13n": "night_snow",
"50n": "night_alt_cloudy_windy"
};
this.currentWeatherType = weatherIconTable[data.weather[0].icon];
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification == "CURRENTWEATHER_DATA") {
this.setCurrentWeatherType(payload.data);
}
},
});

View File

@ -14,7 +14,7 @@ modules: [
config: {
// See 'Configuration options' for more information.
location: 'Amsterdam,Netherlands',
locationID: '', //Location ID from http://bulk.openweather.org/sample/
locationID: '', //Location ID from http://openweathermap.org/help/city_list.txt
appid: 'abcde12345abcde12345abcde12345ab' //openweathermap.org API key.
}
}
@ -35,19 +35,20 @@ The following properties can be configured:
</tr>
<thead>
<tbody>
<tr>
<td><code>location</code></td>
<td>The location used for weather information.<br>
<br><b>Example:</b> <code>Amsterdam,Netherlands</code>
<br><b>Default value:</b> <code>New York</code>
<br><b>Example:</b> <code>'Amsterdam,Netherlands'</code>
<br><b>Default value:</b> <code>false</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td>
</tr>
<tr>
<td><code>locationID</code></td>
<td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<td>Location ID from <a href="http://openweathermap.org/help/city_list.txt">OpenWeatherMap</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<br><b>Example:</b> <code>1234567</code>
<br><b>Default value:</b> <code></code>
<br><b>Default value:</b> <code>false</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td>
</tr>
<tr>
@ -63,18 +64,25 @@ The following properties can be configured:
<br><b>Default value:</b> <code>config.units</code>
</td>
</tr>
<tr>
<td><code>roundTemp</code></td>
<td>Round temperature value to nearest integer.<br>
<br><b>Possible values:</b> <code>true</code> (round to integer) or <code>false</code> (display exact value with decimal point)
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>updateInterval</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> (10 minutes)
<br><b>Default value:</b> <code>600000</code> (10 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)
<br><b>Default value:</b> <code>1000</code> (1 second)
</td>
</tr>
<tr>
@ -105,6 +113,20 @@ The following properties can be configured:
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>showHumidity</code></td>
<td>Show the current humidity<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>onlyTemp</code></td>
<td>Show only current Temperature and weather icon.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>useBeaufort</code></td>
<td>Pick between using the Beaufort scale for wind speed or using the default units.<br>
@ -151,6 +173,18 @@ The following properties can be configured:
<br><b>Default value:</b> <code>'weather'</code>
</td>
</tr>
<tr>
<td><code>appendLocationNameToHeader</code></td>
<td>If set to <code>true</code>, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather.<br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>calendarClass</code></td>
<td>The class for the calender module to base the event based weather information on.<br>
<br><b>Default value:</b> <code>'calendar'</code>
</td>
</tr>
<tr>
<td><code>iconTable</code></td>
<td>The conversion table to convert the weather conditions to weather-icons.<br>

View File

@ -6,3 +6,11 @@
-webkit-transform: translate(0, -3px); /* Safari */
transform: translate(0, -3px);
}
.currentweather .humidityIcon {
padding-right: 4px;
}
.currentweather .humidity-padding {
padding-bottom: 6px;
}

View File

@ -11,8 +11,8 @@ Module.register("currentweather",{
// Default module config.
defaults: {
location: "",
locationID: "",
location: false,
locationID: false,
appid: "",
units: config.units,
updateInterval: 10 * 60 * 1000, // every 10 minutes
@ -23,6 +23,7 @@ Module.register("currentweather",{
showWindDirection: true,
useBeaufort: true,
lang: config.language,
showHumidity: false,
initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500,
@ -31,6 +32,12 @@ Module.register("currentweather",{
apiBase: "http://api.openweathermap.org/data/",
weatherEndpoint: "weather",
appendLocationNameToHeader: true,
calendarClass: "calendar",
onlyTemp: false,
roundTemp: false,
iconTable: {
"01d": "wi-day-sunny",
"02d": "wi-day-cloudy",
@ -53,6 +60,12 @@ Module.register("currentweather",{
},
},
// create a variable for the first upcoming calendaar event. Used if no location is specified.
firstEvent: false,
// create a variable to hold the location name based on the API result.
fetchedLocatioName: "",
// Define required scripts.
getScripts: function() {
return ["moment.js"];
@ -88,35 +101,16 @@ Module.register("currentweather",{
this.loaded = false;
this.scheduleUpdate(this.config.initialLoadDelay);
this.updateTimer = null;
},
// Override dom generator.
getDom: function() {
var wrapper = document.createElement("div");
if (this.config.appid === "") {
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (this.config.location === "") {
wrapper.innerHTML = "Please set the openweather <i>location</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) {
wrapper.innerHTML = this.translate('LOADING');
wrapper.className = "dimmed light small";
return wrapper;
}
// add extra information of current weather
// windDirection, humidity, sunrise and sunset
addExtraInfoWeather: function(wrapper) {
var small = document.createElement("div");
small.className = "normal medium";
var windIcon = document.createElement("span");
windIcon.className = "wi wi-strong-wind dimmed";
small.appendChild(windIcon);
@ -134,6 +128,22 @@ Module.register("currentweather",{
spacer.innerHTML = "&nbsp;";
small.appendChild(spacer);
if (this.config.showHumidity) {
var humidity = document.createElement("span");
humidity.innerHTML = this.humidity;
var spacer = document.createElement("sup");
spacer.innerHTML = "&nbsp;";
var humidityIcon = document.createElement("sup");
humidityIcon.className = "wi wi-humidity humidityIcon";
humidityIcon.innerHTML = "&nbsp;";
small.appendChild(humidity);
small.appendChild(spacer);
small.appendChild(humidityIcon);
}
var sunriseSunsetIcon = document.createElement("span");
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
small.appendChild(sunriseSunsetIcon);
@ -142,6 +152,29 @@ Module.register("currentweather",{
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
small.appendChild(sunriseSunsetTime);
wrapper.appendChild(small);
},
// Override dom generator.
getDom: function() {
var wrapper = document.createElement("div");
if (this.config.appid === "") {
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) {
wrapper.innerHTML = this.translate("LOADING");
wrapper.className = "dimmed light small";
return wrapper;
}
if (this.config.onlyTemp === false) {
this.addExtraInfoWeather(wrapper);
}
var large = document.createElement("div");
large.className = "large light";
@ -154,16 +187,54 @@ Module.register("currentweather",{
temperature.innerHTML = " " + this.temperature + "&deg;";
large.appendChild(temperature);
wrapper.appendChild(small);
wrapper.appendChild(large);
return wrapper;
},
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocatioName;
}
return this.data.header;
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
if (this.config.appendLocationNameToHeader) {
this.hide(0, {lockString: this.identifier});
}
}
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false;
for (e in payload) {
var event = payload[e];
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
break;
}
}
}
}
},
/* updateWeather(compliments)
* Requests new data from openweather.org.
* Calls processWeather on succesfull response.
*/
updateWeather: function() {
if (this.config.appid === "") {
Log.error("CurrentWeather: APPID not set!");
return;
}
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + this.getParams();
var self = this;
var retry = true;
@ -175,11 +246,10 @@ Module.register("currentweather",{
if (this.status === 200) {
self.processWeather(JSON.parse(this.response));
} else if (this.status === 401) {
self.config.appid = "";
self.updateDom(self.config.animationSpeed);
Log.error(self.name + ": Incorrect APPID.");
retry = false;
retry = true;
} else {
Log.error(self.name + ": Could not load weather.");
}
@ -199,11 +269,19 @@ Module.register("currentweather",{
*/
getParams: function() {
var params = "?";
if(this.config.locationID !== "") {
if(this.config.locationID) {
params += "id=" + this.config.locationID;
} else {
} else if(this.config.location) {
params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) {
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
} else if (this.firstEvent && this.firstEvent.location) {
params += "q=" + this.firstEvent.location;
} else {
this.hide(this.config.animationSpeed, {lockString:this.identifier});
return;
}
params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang;
params += "&APPID=" + this.config.appid;
@ -224,6 +302,7 @@ Module.register("currentweather",{
return;
}
this.humidity = parseFloat(data.main.humidity);
this.temperature = this.roundValue(data.main.temp);
if (this.config.useBeaufort){
@ -244,20 +323,20 @@ Module.register("currentweather",{
// So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181
var sunriseSunsetDateObject = (sunrise < now && sunset > now) ? sunset : sunrise;
var timeString = moment(sunriseSunsetDateObject).format('HH:mm');
var timeString = moment(sunriseSunsetDateObject).format("HH:mm");
if (this.config.timeFormat !== 24) {
//var hours = sunriseSunsetDateObject.getHours() % 12 || 12;
if (this.config.showPeriod) {
if (this.config.showPeriodUpper) {
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm A');
timeString = moment(sunriseSunsetDateObject).format('h:mm A');
timeString = moment(sunriseSunsetDateObject).format("h:mm A");
} else {
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm a');
timeString = moment(sunriseSunsetDateObject).format('h:mm a');
timeString = moment(sunriseSunsetDateObject).format("h:mm a");
}
} else {
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm');
timeString = moment(sunriseSunsetDateObject).format('h:mm');
timeString = moment(sunriseSunsetDateObject).format("h:mm");
}
}
@ -265,9 +344,10 @@ Module.register("currentweather",{
this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise";
this.show(this.config.animationSpeed, {lockString:this.identifier});
this.loaded = true;
this.updateDom(this.config.animationSpeed);
this.sendNotification("CURRENTWEATHER_DATA", {data: data});
},
/* scheduleUpdate()
@ -306,52 +386,51 @@ Module.register("currentweather",{
return 12;
},
deg2Cardinal: function(deg) {
if (deg>11.25 && deg<=33.75){
return "NNE";
} else if (deg > 33.75 && deg <= 56.25) {
return "NE";
} else if (deg > 56.25 && deg <= 78.75) {
return "ENE";
} else if (deg > 78.75 && deg <= 101.25) {
return "E";
} else if (deg > 101.25 && deg <= 123.75) {
return "ESE";
} else if (deg > 123.75 && deg <= 146.25) {
return "SE";
} else if (deg > 146.25 && deg <= 168.75) {
return "SSE";
} else if (deg > 168.75 && deg <= 191.25) {
return "S";
} else if (deg > 191.25 && deg <= 213.75) {
return "SSW";
} else if (deg > 213.75 && deg <= 236.25) {
return "SW";
} else if (deg > 236.25 && deg <= 258.75) {
return "WSW";
} else if (deg > 258.75 && deg <= 281.25) {
return "W";
} else if (deg > 281.25 && deg <= 303.75) {
return "WNW";
} else if (deg > 303.75 && deg <= 326.25) {
return "NW";
} else if (deg > 326.25 && deg <= 348.75) {
return "NNW";
} else {
return "N";
}
},
/* function(temperature)
* Rounds a temperature to 1 decimal.
* Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
*
* argument temperature number - Temperature.
*
* return number - Rounded Temperature.
*/
deg2Cardinal: function(deg) {
if (deg>11.25 && deg<=33.75){
return "NNE";
} else if (deg > 33.75 && deg <= 56.25) {
return "NE";
} else if (deg > 56.25 && deg <= 78.75) {
return "ENE";
} else if (deg > 78.75 && deg <= 101.25) {
return "E";
} else if (deg > 101.25 && deg <= 123.75) {
return "ESE";
} else if (deg > 123.75 && deg <= 146.25) {
return "SE";
} else if (deg > 146.25 && deg <= 168.75) {
return "SSE";
} else if (deg > 168.75 && deg <= 191.25) {
return "S";
} else if (deg > 191.25 && deg <= 213.75) {
return "SSW";
} else if (deg > 213.75 && deg <= 236.25) {
return "SW";
} else if (deg > 236.25 && deg <= 258.75) {
return "WSW";
} else if (deg > 258.75 && deg <= 281.25) {
return "W";
} else if (deg > 281.25 && deg <= 303.75) {
return "WNW";
} else if (deg > 303.75 && deg <= 326.25) {
return "NW";
} else if (deg > 326.25 && deg <= 348.75) {
return "NNW";
} else {
return "N";
}
},
roundValue: function(temperature) {
return parseFloat(temperature).toFixed(1);
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
}
});

View File

@ -15,7 +15,8 @@ var defaultModules = [
"currentweather",
"helloworld",
"newsfeed",
"weatherforecast"
"weatherforecast",
"updatenotification"
];
/*************** DO NOT EDIT THE LINE BELOW ***************/

View File

@ -53,6 +53,7 @@ The following properties can be configured:
{
title: "New York Times",
url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml",
encoding: "UTF-8"
}
]</code>
</td>
@ -98,7 +99,7 @@ The following properties can be configured:
<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)
<br><b>Default value:</b> <code>2500</code> (2.5 seconds)
</td>
</tr>
<tr>
@ -112,13 +113,13 @@ The following properties can be configured:
removeEndTags: false,
startTags: [],
endTags: []
<tr>
<td><code>removeStartTags</code></td>
<td>Some newsfeeds feature tags at the <B>beginning</B> of their titles or descriptions, such as <em>[VIDEO]</em>.
This setting allows for the removal of specified tags from the beginning of an item's description and/or title.<br>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
</td>
</tr>
<tr>
@ -130,7 +131,7 @@ The following properties can be configured:
<tr>
<td><code>removeEndTags</code></td>
<td>Remove specified tags from the <B>end</B> of an item's description and/or title.<br>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
</td>
</tr>
<tr>
@ -172,7 +173,7 @@ The `feeds` property contains an array with multiple objects. These objects have
<td>The encoding of the news feed.<br>
<br>This property is optional.
<br><b>Possible values:</b><code>'UTF-8'</code>, <code>'ISO-8859-1'</code>, etc ...
<br><b>Default value:</b> <code>'UTF-8'</code>
<br><b>Default value:</b> <code>'UTF-8'</code>
</td>
</tr>

View File

@ -44,8 +44,9 @@ var Fetcher = function(url, reloadInterval, encoding) {
parser.on("item", function(item) {
var title = item.title;
var description = item.description || item.summary || item.content || '';
var description = item.description || item.summary || item.content || "";
var pubdate = item.pubdate || item.published || item.updated;
var url = item.url || item.link || "";
if (title && pubdate) {
@ -56,15 +57,16 @@ var Fetcher = function(url, reloadInterval, encoding) {
title: title,
description: description,
pubdate: pubdate,
url: url,
});
} else {
console.log("Can't parse feed item:");
console.log(item);
console.log('Title: ' + title);
console.log('Description: ' + description);
console.log('Pubdate: ' + pubdate);
// console.log("Can't parse feed item:");
// console.log(item);
// console.log('Title: ' + title);
// console.log('Description: ' + description);
// console.log('Pubdate: ' + pubdate);
}
});
@ -79,13 +81,11 @@ var Fetcher = function(url, reloadInterval, encoding) {
scheduleTimer();
});
var headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A'};
request({uri: url, encoding: null, headers: headers})
.on('error', function(error) {
fetchFailedCallback(self, error);
scheduleTimer();
})
.pipe(iconv.decodeStream(encoding)).pipe(parser);
nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
headers = {"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"}
request({uri: url, encoding: null, headers: headers}).pipe(iconv.decodeStream(encoding)).pipe(parser);
};

View File

@ -25,11 +25,11 @@ Module.register("newsfeed",{
updateInterval: 10 * 1000,
animationSpeed: 2.5 * 1000,
maxNewsItems: 0, // 0 for unlimited
removeStartTags: '',
removeEndTags: '',
removeStartTags: "",
removeEndTags: "",
startTags: [],
endTags: []
},
// Define required scripts.
@ -93,62 +93,70 @@ Module.register("newsfeed",{
var sourceAndTimestamp = document.createElement("div");
sourceAndTimestamp.className = "light small dimmed";
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '') sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '' && this.config.showPublishDate) sourceAndTimestamp.innerHTML += ', ';
if (this.config.showPublishDate) sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow();
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '' || this.config.showPublishDate) sourceAndTimestamp.innerHTML += ':';
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
}
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += ", ";
}
if (this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow();
}
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" || this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += ":";
}
wrapper.appendChild(sourceAndTimestamp);
}
//Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags == 'title' || 'both') {
if (this.config.removeStartTags == "title" || "both") {
for (f=0; f<this.config.startTags.length;f++) {
if (this.newsItems[this.activeItem].title.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].title.length);
}
}
}
}
if (this.config.removeStartTags == 'description' || 'both') {
if (this.config.removeStartTags == "description" || "both") {
if (this.config.showDescription) {
for (f=0; f<this.config.startTags.length;f++) {
if (this.newsItems[this.activeItem].description.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
}
}
}
}
}
//Remove selected tags from the end of rss feed items (title or description)
if (this.config.removeEndTags) {
for (f=0; f<this.config.endTags.length;f++) {
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0,-this.config.endTags[f].length);
}
}
}
if (this.config.showDescription) {
for (f=0; f<this.config.endTags.length;f++) {
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0,-this.config.endTags[f].length);
}
}
}
}
}
var title = document.createElement("div");
title.className = "bright medium light";
title.innerHTML = this.newsItems[this.activeItem].title;
wrapper.appendChild(title);
if (this.config.showDescription) {
var description = document.createElement("div");
description.className = "small light";
@ -234,10 +242,10 @@ Module.register("newsfeed",{
for (var f in this.config.feeds) {
var feed = this.config.feeds[f];
if (feed.url === feedUrl) {
return feed.title || '';
return feed.title || "";
}
}
return '';
return "";
},
/* scheduleUpdateInterval()

View File

@ -35,8 +35,8 @@ module.exports = NodeHelper.create({
createFetcher: function(feed, config) {
var self = this;
var url = feed.url || '';
var encoding = feed.encoding || 'UTF-8';
var url = feed.url || "";
var encoding = feed.encoding || "UTF-8";
var reloadInterval = config.reloadInterval || 5 * 60 * 1000;
if (!validUrl.isUri(url)) {
@ -72,7 +72,7 @@ module.exports = NodeHelper.create({
},
/* broadcastFeeds()
* Creates an object with all feed items of the different registered feeds,
* Creates an object with all feed items of the different registered feeds,
* and broadcasts these using sendSocketNotification.
*/
broadcastFeeds: function() {

View File

@ -0,0 +1,42 @@
# Module: Update Notification
The `updatenotification` module is one of the default modules of the MagicMirror.
This will display a message whenever a new version of the MagicMirror application is available.
## Using the module
To use this module, add it to the modules array in the `config/config.js` file:
````javascript
modules: [
{
module: 'updatenotification',
position: 'top_center', // This can be any of the regions.
config: {
// The config property is optional.
// 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>updateInterval</code></td>
<td>How often do you want to check for a new version? This value represents the interval in milliseconds.<br>
<br><b>Possible values:</b> Any value above <code>60000</code> (1 minute);
<br><b>Default value:</b> <code>600000</code> (10 minutes);
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,87 @@
var SimpleGit = require("simple-git");
var simpleGits = [];
var fs = require("fs");
var path = require("path");
var defaultModules = require(__dirname + "/../defaultmodules.js");
var NodeHelper = require("node_helper");
module.exports = NodeHelper.create({
config: {},
updateTimer: null,
start: function () {
},
configureModules: function(modules) {
for (moduleName in modules) {
if (defaultModules.indexOf(moduleName) < 0) {
// Default modules are included in the main MagicMirror repo
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
var stat;
try {
stat = fs.statSync(path.join(moduleFolder, ".git"));
} catch(err) {
// Error when directory .git doesn't exist
// This module is not managed with git, skip
continue;
}
var res = function(mn, mf) {
var git = SimpleGit(mf);
git.getRemotes(true, function(err, remotes) {
if (remotes.length < 1 || remotes[0].name.length < 1) {
// No valid remote for folder, skip
return;
}
// Folder has .git and has at least one git remote, watch this folder
simpleGits.push({"module": mn, "git": git});
});
}(moduleName, moduleFolder);
}
}
// Push MagicMirror itself last, biggest chance it'll show up last in UI and isn't overwritten
simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))});
},
socketNotificationReceived: function (notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
} else if(notification === "MODULES") {
this.configureModules(payload);
this.preformFetch();
}
},
preformFetch() {
var self = this;
simpleGits.forEach(function(sg) {
sg.git.fetch().status(function(err, data) {
data.module = sg.module;
if (!err) {
self.sendSocketNotification("STATUS", data);
}
});
});
this.scheduleNextFetch(this.config.updateInterval);
},
scheduleNextFetch: function(delay) {
if (delay < 60 * 1000) {
delay = 60 * 1000
}
var self = this;
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function() {
self.preformFetch();
}, delay);
}
});

View File

@ -0,0 +1,70 @@
Module.register("updatenotification", {
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
},
status: false,
start: function () {
Log.log("Start updatenotification");
},
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Module.definitions);
this.hide(0,{lockString: self.identifier});
}
},
socketNotificationReceived: function (notification, payload) {
if (notification === "STATUS") {
this.status = payload;
this.updateUI();
}
},
updateUI: function() {
var self = this;
if (this.status && this.status.behind > 0) {
self.updateDom(0);
self.show(1000, {lockString: self.identifier});
}
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
if (this.status && this.status.behind > 0) {
var message = document.createElement("div");
message.className = "small bright";
var icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var text = document.createElement("span");
if (this.status.module == "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
} else {
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE").replace("MODULE_NAME", this.status.module);
}
message.appendChild(text);
wrapper.appendChild(message);
var subtext = document.createElement("div");
subtext.innerHTML = this.translate("UPDATE_INFO")
.replace("COMMIT_COUNT", this.status.behind + " " + ((this.status.behind == 1)? "commit" : "commits"))
.replace("BRANCH_NAME", this.status.current);
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);
}
return wrapper;
}
});

View File

@ -14,7 +14,7 @@ modules: [
config: {
// See 'Configuration options' for more information.
location: 'Amsterdam,Netherlands',
locationID: '', //Location ID from http://bulk.openweather.org/sample/
locationID: '', //Location ID from http://openweathermap.org/help/city_list.txt
appid: 'abcde12345abcde12345abcde12345ab' //openweathermap.org API key.
}
}
@ -35,19 +35,20 @@ The following properties can be configured:
</tr>
<thead>
<tbody>
<tr>
<td><code>location</code></td>
<td>The location used for weather information.<br>
<br><b>Example:</b> <code>Amsterdam,Netherlands</code>
<br><b>Default value:</b> <code>New York</code>
<br><b>Example:</b> <code>'Amsterdam,Netherlands'</code>
<br><b>Default value:</b> <code>false</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td>
</tr>
<tr>
<td><code>locationID</code></td>
<td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<td>Location ID from <a href="http://openweathermap.org/help/city_list.txt">OpenWeatherMap</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<br><b>Example:</b> <code>1234567</code>
<br><b>Default value:</b> <code></code>
<br><b>Default value:</b> <code>false</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td>
</tr>
<tr>
@ -63,6 +64,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>config.units</code>
</td>
</tr>
<tr>
<td><code>roundTemp</code></td>
<td>Round temperature values to nearest integer.<br>
<br><b>Possible values:</b> <code>true</code> (round to integer) or <code>false</code> (display exact value with decimal point)
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>maxNumberOfDays</code></td>
<td>How many days of forecast to return. Specified by config.js<br>
@ -71,21 +79,28 @@ The following properties can be configured:
<br>This value is optional. By default the weatherforecast module will return 7 days.
</td>
</tr>
<tr>
<td><code>showRainAmount</code></td>
<td>Should the predicted rain amount be displayed?<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
<br>This value is optional. By default the weatherforecast module will not display the predicted amount of rain.
</td>
</tr>
<tr>
<td><code>updateInterval</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> (10 minutes)
<br><b>Default value:</b> <code>600000</code> (10 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)
<br><b>Default value:</b> <code>1000</code> (1 second)
</td>
</tr>
<tr>
<td><code>lang</code></td>
<td>The language of the days.<br>
@ -111,7 +126,7 @@ The following properties can be configured:
<td><code>initialLoadDelay</code></td>
<td>The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds)<br>
<br><b>Possible values:</b> <code>1000</code> - <code>5000</code>
<br><b>Default value:</b> <code>0</code>
<br><b>Default value:</b> <code>2500</code> (2.5 seconds delay. This delay is used to keep the OpenWeather API happy.)
</td>
</tr>
<tr>
@ -134,11 +149,23 @@ The following properties can be configured:
</td>
</tr>
<tr>
<td><code>weatherEndpoint</code></td>
<td><code>forecastEndpoint</code></td>
<td>The OpenWeatherMap API endPoint.<br>
<br><b>Default value:</b> <code>'forecast/daily'</code>
</td>
</tr>
<tr>
<td><code>appendLocationNameToHeader</code></td>
<td>If set to <code>true</code>, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather.<br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>calendarClass</code></td>
<td>The class for the calender module to base the event based weather information on.<br>
<br><b>Default value:</b> <code>'calendar'</code>
</td>
</tr>
<tr>
<td><code>iconTable</code></td>
<td>The conversion table to convert the weather conditions to weather-icons.<br>
@ -164,6 +191,5 @@ The following properties can be configured:
}</code>
</td>
</tr>
</tbody>
</table>

View File

@ -12,3 +12,8 @@
padding-left: 20px;
padding-right: 0;
}
.weatherforecast .rain {
padding-left: 20px;
padding-right: 0;
}

View File

@ -11,11 +11,12 @@ Module.register("weatherforecast",{
// Default module config.
defaults: {
location: "",
locationID: "",
location: false,
locationID: false,
appid: "",
units: config.units,
maxNumberOfDays: 7,
showRainAmount: false,
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
timeFormat: config.timeFormat,
@ -30,6 +31,11 @@ Module.register("weatherforecast",{
apiBase: "http://api.openweathermap.org/data/",
forecastEndpoint: "forecast/daily",
appendLocationNameToHeader: true,
calendarClass: "calendar",
roundTemp: false,
iconTable: {
"01d": "wi-day-sunny",
"02d": "wi-day-cloudy",
@ -52,6 +58,12 @@ Module.register("weatherforecast",{
},
},
// create a variable for the first upcoming calendaar event. Used if no location is specified.
firstEvent: false,
// create a variable to hold the location name based on the API result.
fetchedLocatioName: "",
// Define required scripts.
getScripts: function() {
return ["moment.js"];
@ -95,12 +107,6 @@ Module.register("weatherforecast",{
return wrapper;
}
if (this.config.location === "") {
wrapper.innerHTML = "Please set the openweather <i>location</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) {
wrapper.innerHTML = this.translate("LOADING");
wrapper.className = "dimmed light small";
@ -139,6 +145,17 @@ Module.register("weatherforecast",{
minTempCell.className = "align-right min-temp";
row.appendChild(minTempCell);
if (this.config.showRainAmount) {
var rainCell = document.createElement("td");
if (isNaN(forecast.rain)) {
rainCell.innerHTML = "";
} else {
rainCell.innerHTML = forecast.rain + " mm";
}
rainCell.className = "align-right bright rain";
row.appendChild(rainCell);
}
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
@ -156,11 +173,50 @@ Module.register("weatherforecast",{
return table;
},
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocatioName;
}
return this.data.header;
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
if (this.config.appendLocationNameToHeader) {
this.hide(0, {lockString: this.identifier});
}
}
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false;
for (e in payload) {
var event = payload[e];
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
break;
}
}
}
}
},
/* updateWeather(compliments)
* Requests new data from openweather.org.
* Calls processWeather on succesfull response.
*/
updateWeather: function() {
if (this.config.appid === "") {
Log.error("WeatherForecast: APPID not set!");
return;
}
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + this.getParams();
var self = this;
var retry = true;
@ -172,11 +228,10 @@ Module.register("weatherforecast",{
if (this.status === 200) {
self.processWeather(JSON.parse(this.response));
} else if (this.status === 401) {
self.config.appid = "";
self.updateDom(self.config.animationSpeed);
Log.error(self.name + ": Incorrect APPID.");
retry = false;
retry = true;
} else {
Log.error(self.name + ": Could not load weather.");
}
@ -196,11 +251,19 @@ Module.register("weatherforecast",{
*/
getParams: function() {
var params = "?";
if(this.config.locationID !== "") {
if(this.config.locationID) {
params += "id=" + this.config.locationID;
} else {
} else if(this.config.location) {
params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) {
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
} else if (this.firstEvent && this.firstEvent.location) {
params += "q=" + this.firstEvent.location;
} else {
this.hide(this.config.animationSpeed, {lockString:this.identifier});
return;
}
params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang;
/*
@ -220,6 +283,7 @@ Module.register("weatherforecast",{
* argument data object - Weather information received form openweather.org.
*/
processWeather: function(data) {
this.fetchedLocatioName = data.city.name + ", " + data.city.country;
this.forecast = [];
for (var i = 0, count = data.list.length; i < count; i++) {
@ -230,13 +294,14 @@ Module.register("weatherforecast",{
day: moment(forecast.dt, "X").format("ddd"),
icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min)
minTemp: this.roundValue(forecast.temp.min),
rain: this.roundValue(forecast.rain)
});
}
//Log.log(this.forecast);
this.show(this.config.animationSpeed, {lockString:this.identifier});
this.loaded = true;
this.updateDom(this.config.animationSpeed);
},
@ -279,13 +344,14 @@ Module.register("weatherforecast",{
},
/* function(temperature)
* Rounds a temperature to 1 decimal.
* Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
*
* argument temperature number - Temperature.
*
* return number - Rounded Temperature.
*/
roundValue: function(temperature) {
return parseFloat(temperature).toFixed(1);
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
}
});

View File

@ -1,13 +1,10 @@
{
"name": "magicmirror",
"version": "2.0.0",
"version": "2.1.0",
"description": "A modular interface for smart mirrors.",
"main": "js/electron.js",
"scripts": {
"start": "electron js/electron.js",
"test": "snyk test",
"snyk-protect": "snyk protect",
"prepublish": "npm run snyk-protect"
"start": "electron js/electron.js"
},
"repository": {
"type": "git",
@ -20,7 +17,9 @@
"modular"
],
"author": "Michael Teeuw",
"contributors": "https://github.com/MichMich/MagicMirror/graphs/contributors",
"contributors": [
"https://github.com/MichMich/MagicMirror/graphs/contributors"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/MichMich/MagicMirror/issues"
@ -30,24 +29,25 @@
"grunt": "latest",
"grunt-eslint": "latest",
"grunt-jsonlint": "latest",
"grunt-markdownlint": "^1.0.4",
"grunt-markdownlint": "^1.0.13",
"grunt-stylelint": "latest",
"grunt-yamllint": "latest",
"stylelint-config-standard": "latest",
"time-grunt": "latest"
},
"dependencies": {
"electron-prebuilt": "latest",
"electron": "^1.4.7",
"express": "^4.14.0",
"express-ipfilter": "latest",
"feedme": "latest",
"helmet": "^3.1.0",
"iconv-lite": "latest",
"moment": "latest",
"request": "^2.74.0",
"request": "^2.78.0",
"rrule": "latest",
"socket.io": "^1.4.6",
"simple-git": "^1.62.0",
"socket.io": "^1.5.1",
"valid-url": "latest",
"walk": "latest",
"snyk": "^1.14.1"
},
"snyk": true
"walk": "latest"
}
}

View File

@ -1,5 +1,6 @@
var app = require("../js/app.js");
app.start(function(config) {
console.log("");
console.log("Ready to go! Please point your browser to: http://localhost:" + config.port);
var bindAddress = config.address ? config.address : "localhost";
console.log("Ready to go! Please point your browser to: http://" + bindAddress + ":" + config.port);
});

View File

@ -0,0 +1,8 @@
[Plymouth Theme]
Name=MagicMirror
Description=Mirror Splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/MagicMirror
ScriptFile=/usr/share/plymouth/themes/MagicMirror/MagicMirror.script

View File

@ -0,0 +1,48 @@
screen_width = Window.GetWidth();
screen_height = Window.GetHeight();
theme_image = Image("splash.png");
image_width = theme_image.GetWidth();
image_height = theme_image.GetHeight();
scale_x = image_width / screen_width;
scale_y = image_height / screen_height;
if (scale_x > 1 || scale_y > 1)
{
if (scale_x > scale_y)
{
resized_image = theme_image.Scale (screen_width, image_height / scale_x);
image_x = 0;
image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2;
}
else
{
resized_image = theme_image.Scale (image_width / scale_y, screen_height);
image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2;
image_y = 0;
}
}
else
{
resized_image = theme_image.Scale (image_width, image_height);
image_x = (screen_width - image_width) / 2;
image_y = (screen_height - image_height) / 2;
}
if (Plymouth.GetMode() != "shutdown")
{
sprite = Sprite (resized_image);
sprite.SetPosition (image_x, image_y, -100);
}
message_sprite = Sprite();
message_sprite.SetPosition(screen_width * 0.1, screen_height * 0.9, 10000);
fun message_callback (text) {
my_image = Image.Text(text, 1, 1, 1);
message_sprite.SetImage(my_image);
sprite.SetImage (resized_image);
}
Plymouth.SetUpdateStatusFunction(message_callback);

BIN
splashscreen/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

35
translations/da.json Normal file
View File

@ -0,0 +1,35 @@
{
/* GENERAL */
"LOADING": "Indlæser &hellip;",
/* CALENDAR */
"TODAY": "I dag",
"TOMORROW": "I morgen",
"DAYAFTERTOMORROW": "I overmorgen",
"RUNNING": "Slutter om",
"EMPTY": "Ingen kommende begivenheder.",
/* WEATHER */
"N": "N",
"NNE": "NNØ",
"NE": "NØ",
"ENE": "ØNØ",
"E": "Ø",
"ESE": "ØSØ",
"SE": "SØ",
"SSE": "SSØ",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² opdatering tilgængelig.",
"UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for MODULE_NAME modulet.",
"UPDATE_INFO": "Den nuværende installation er COMMIT_COUNT bagud på BRANCH_NAME branch'en."
}

View File

@ -10,20 +10,25 @@
"EMPTY": "Keine Termine.",
/* WEATHER */
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "O",
"ESE": "OSO",
"SE": "SO",
"SSE": "SSO",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "O",
"ESE": "OSO",
"SE": "SO",
"SSE": "SSO",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das MODULE_NAME Modul verfügbar.",
"UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch."
}

View File

@ -5,24 +5,30 @@
/* CALENDAR */
"TODAY": "Today",
"TOMORROW": "Tomorrow",
"DAYAFTERTOMORROW": "The day after tomorrow",
"RUNNING": "Ends in",
"EMPTY": "No upcoming events.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
}

View File

@ -5,24 +5,30 @@
/* CALENDAR */
"TODAY": "Hoy",
"TOMORROW": "Mañana",
"DAYAFTERTOMORROW": "Pasado mañana",
"RUNNING": "Termina en",
"EMPTY": "No hay eventos programados.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo MODULE_NAME.",
"UPDATE_INFO": "Tu actual instalación está COMMIT_COUNT cambios detrás de la rama BRANCH_NAME."
}

28
translations/fi.json Normal file
View File

@ -0,0 +1,28 @@
{
/* GENERAL */
"LOADING": "Lataa &hellip;",
/* CALENDAR */
"TODAY": "Tänään",
"TOMORROW": "Huomenna",
"RUNNING": "Meneillään",
"EMPTY": "Ei tulevia tapahtumia.",
/* WEATHER */
"N": "P",
"NNE": "PPI",
"NE": "PI",
"ENE": "IPI",
"E": "I",
"ESE": "IEI",
"SE": "EI",
"SSE": "EEI",
"S": "E",
"SSW": "EEL",
"SW": "EL",
"WSW": "LEL",
"W": "L",
"WNW": "LPL",
"NW": "PL",
"NNW": "PPL"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Aucun RDV.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
}

View File

@ -5,24 +5,25 @@
/* CALENDAR */
"TODAY": "Hjoed",
"TOMORROW": "Moarn",
"DAYAFTERTOMORROW": "Oaremoarn",
"RUNNING": "Einigest oer",
"EMPTY": "Gjin plande &ocirc;fspraken.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Nessun evento in arrivo.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
}

View File

@ -1,28 +1,28 @@
{
/* GENERAL */
"LOADING": "ローディング &hellip;",
"LOADING": "ローディング &hellip;",
/* CALENDAR */
"TODAY":"今日",
"TOMORROW":"明日",
"RUNNING":"で終わります",
"EMPTY":"直近のイベントはありません",
/* CALENDAR */
"TODAY": "今日",
"TOMORROW": "明日",
"RUNNING": "で終わります",
"EMPTY": "直近のイベントはありません",
/* WEATHER */
"N":"北",
"NNE":"北北東",
"NE":"北東",
"ENE":"東北東",
"E":"東",
"ESE":"東南東",
"SE":"南東",
"SSE":"南南東",
"S":"南",
"SSW":"南南西",
"SW":"南西",
"WSW":"西南西",
"W":"西",
"WNW":"西北西",
"NW":"北西",
"NNW":"北北西"
/* WEATHER */
"N": "北",
"NNE": "北北東",
"NE": "北東",
"ENE": "東北東",
"E": "東",
"ESE": "東南東",
"SE": "南東",
"SSE": "南南東",
"S": "南",
"SSW": "南南西",
"SW": "南西",
"WSW": "西南西",
"W": "西",
"WNW": "西北西",
"NW": "北西",
"NNW": "北北西"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Ingen kommende arrangementer.",
/* WEATHER */
"N": "N",
"NNE": "NNØ",
"NE": "NØ",
"ENE": "ØNØ",
"E": "Ø",
"ESE": "ØSØ",
"SE": "SØ",
"SSE": "SSØ",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
"N": "N",
"NNE": "NNØ",
"NE": "NØ",
"ENE": "ØNØ",
"E": "Ø",
"ESE": "ØSØ",
"SE": "SØ",
"SSE": "SSØ",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
}

View File

@ -10,20 +10,25 @@
"EMPTY": "Geen geplande afspraken.",
/* WEATHER */
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "O",
"ESE": "OZO",
"SE": "ZO",
"SSE": "ZZO",
"S": "Z",
"SSW": "ZZW",
"SW": "ZW",
"WSW": "WZW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "O",
"ESE": "OZO",
"SE": "ZO",
"SSE": "ZZO",
"S": "Z",
"SSW": "ZZW",
"SW": "ZW",
"WSW": "WZW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor MODULE_NAME module.",
"UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch."
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Ingen komande hendingar.",
/* WEATHER */
"N": "N",
"NNE": "NNA",
"NE": "NA",
"ENE": "ANA",
"E": "A",
"ESE": "ASA",
"SE": "SA",
"SSE": "SSA",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
"N": "N",
"NNE": "NNA",
"NE": "NA",
"ENE": "ANA",
"E": "A",
"ESE": "ASA",
"SE": "SA",
"SSE": "SSA",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
}

View File

@ -9,20 +9,25 @@
"EMPTY": "Brak wydarzeń.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "Dostępna jest aktualizacja MagicMirror².",
"UPDATE_NOTIFICATION_MODULE": "Dostępna jest aktualizacja modułu MODULE_NAME.",
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Sem eventos a chegar.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Nenhum evento novo.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "Inga kommande händelser.",
/* WEATHER */
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "Ö",
"ESE": "OSO",
"SE": "SO",
"SSE": "SSO",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
"N": "N",
"NNE": "NNO",
"NE": "NO",
"ENE": "ONO",
"E": "Ö",
"ESE": "OSO",
"SE": "SO",
"SSE": "SSO",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
}

28
translations/tr.json Normal file
View File

@ -0,0 +1,28 @@
{
/* GENERAL */
"LOADING": "Yükleniyor &hellip;",
/* CALENDAR */
"TODAY": "Bugün",
"TOMORROW": "Yarın",
"RUNNING": "Biten",
"EMPTY": "Yakında etkinlik yok.",
/* WEATHER */
"N": "K",
"NNE": "KKD",
"NE": "KD",
"ENE": "DKD",
"E": "D",
"ESE": "DGD",
"SE": "GD",
"SSE": "GGD",
"S": "G",
"SSW": "GGB",
"SW": "GB",
"WSW": "BGB",
"W": "B",
"WNW": "BKB",
"NW": "KB",
"NNW": "KKB"
}

View File

@ -9,6 +9,7 @@ var translations = {
"en" : "translations/en.json", // English
"nl" : "translations/nl.json", // Dutch
"de" : "translations/de.json", // German
"fi" : "translations/fi.json", // Suomi
"fr" : "translations/fr.json", // French
"fy" : "translations/fy.json", // Frysk
"es" : "translations/es.json", // Spanish
@ -23,4 +24,6 @@ var translations = {
"ja" : "translations/ja.json", // Japanese
"pl" : "translations/pl.json", // Polish
"gr" : "translations/gr.json", // Greek
"da" : "translations/da.json", // Danish
"tr" : "translations/tr.json", // Turkish
};

View File

@ -9,20 +9,20 @@
"EMPTY": "没有更多的活动。",
/* WEATHER */
"N": "北风",
"NNE": "北偏东风",
"NE": "东北风",
"ENE": "东偏北风",
"E": "东风",
"ESE": "东偏南风",
"SE": "东南风",
"SSE": "南偏东风",
"S": "南风",
"SSW": "南偏西风",
"SW": "西南风",
"WSW": "西偏南风",
"W": "西风",
"WNW": "西偏北风",
"NW": "西北风",
"NNW": "北偏西风"
"N": "北风",
"NNE": "北偏东风",
"NE": "东北风",
"ENE": "东偏北风",
"E": "东风",
"ESE": "东偏南风",
"SE": "东南风",
"SSE": "南偏东风",
"S": "南风",
"SSW": "南偏西风",
"SW": "西南风",
"WSW": "西偏南风",
"W": "西风",
"WNW": "西偏北风",
"NW": "西北风",
"NNW": "北偏西风"
}

View File

@ -9,20 +9,20 @@
"EMPTY": "沒有更多的活動。",
/* WEATHER */
"N": "北風",
"NNE": "北偏東風",
"NE": "東北風",
"ENE": "東偏北風",
"E": "東風",
"ESE": "東偏南風",
"SE": "東南風",
"SSE": "南偏東風",
"S": "南風",
"SSW": "南偏西風",
"SW": "西南風",
"WSW": "西偏南風",
"W": "西風",
"WNW": "西偏北風",
"NW": "西北風",
"NNW": "北偏西風"
"N": "北風",
"NNE": "北偏東風",
"NE": "東北風",
"ENE": "東偏北風",
"E": "東風",
"ESE": "東偏南風",
"SE": "東南風",
"SSE": "南偏東風",
"S": "南風",
"SSW": "南偏西風",
"SW": "西南風",
"WSW": "西偏南風",
"W": "西風",
"WNW": "西偏北風",
"NW": "西北風",
"NNW": "北偏西風"
}

1197
vendor/moment/moment-timezone.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
vendor/vendor.js vendored
View File

@ -9,6 +9,7 @@
var vendor = {
'moment.js' : 'moment/moment-with-locales.js',
'moment-timezone.js' : 'moment/moment-timezone.js',
'weather-icons.css': 'weather-icons/css/weather-icons.css',
'weather-icons-wind.css': 'weather-icons/css/weather-icons-wind.css',
'font-awesome.css': 'font-awesome-4.5.0/css/font-awesome.min.css'