diff --git a/.eslintignore b/.eslintignore index 25422889..2e1f92d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ vendor/ !/vendor/vendor.js -/modules/** !/modules/default/** !/modules/node_helper !/modules/node_helper/** diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7025dd7c..ee3b7219 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,57 +3,47 @@ Contribution Policy for MagicMirror² Thanks for contributing to MagicMirror²! -We hold our code to standard, and these standards are documented below. +We hold our code to standard, and these standards are documented below. -First, before you run the linters, you will need to install them all **and** install the development dependencies: +If you wish to run both linters, use `grunt` without any arguments. -```bash -(sudo) npm install -g jscs stylelint html-validator-cli -npm install -``` +### JavaScript: Run ESLint -### JavaScript: Run JSCS +We use [ESLint](http://eslint.org) on our JavaScript files. -We use [JSCS](http://jscs.info) on our JavaScript files. +Our ESLint configuration is in our .eslintrc.json and .eslintignore files. -Our JSCS configuration is in our .jscsrc file. - -To run JSCS, use `npm run jscs`. +To run ESLint, use `grunt eslint`. ### CSS: Run StyleLint We use [StyleLint](http://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file. -To run StyleLint, use `npm run stylelint`. +To run StyleLint, use `grunt stylelint`. -### HTML: Run HTML Validator - -We use [NU Validator](https://validator.w3.org/nu) to validate our HTML. The configuration is in the command in the package.json file. - -To run HTML Validator, use `npm run htmlvalidator`. - -## Submitting Issues +### Submitting 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 - Problems installing or configuring your MagicMirror? Check out: https://forum.magicmirror.builders/category/10/troubleshooting +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) When submitting a new issue, please supply the following information: -**Platform** [ Raspberry Pi 2/3, Windows, Mac OS X, Linux, Etc ... ]: +**Platform**: Place your platform here... give us your web browser/Electron version *and* your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX). -**Node Version** [ 0.12.13 or later ]: +**Node Version**: Make sure it's version 0.12.13 or later. -**MagicMirror Version** [ V1 / V2-Beta ]: +**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2). -**Description:** Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. +**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. -**Steps to Reproduce:** List the step by step process to reproduce the issue. +**Steps to Reproduce**: List the step by step process to reproduce the issue. -**Expected Results:** Describe what you expected to see. +**Expected Results**: Describe what you expected to see. -**Actual Results:** Describe what you actually saw. +**Actual Results**: Describe what you actually saw. -**Configuration:** What does the used config.js file look like? (Don't forget to remove any sensitive information.) +**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information! -**Additional Notes:** Provide any other relevant notes not previously mentioned (optional) +**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6d039bf1..736795d7 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,21 +1,24 @@ 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 - Problems installing or configuring your MagicMirror? Check out: https://forum.magicmirror.builders/category/10/troubleshooting +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) -**Platform** [ Raspberry Pi 2/3, Windows, Mac OS X, Linux, Etc ... ]: +When submitting a new issue, please supply the following information: -**Node Version** [ 0.12.13 or later ]: +**Platform**: Place your platform here... give us your web browser/Electron version *and* your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX). -**MagicMirror Version** [ V1 / V2-Beta ]: +**Node Version**: Make sure it's version 0.12.13 or later. -**Description:** Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. +**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2). -**Steps to Reproduce:** List the step by step process to reproduce the issue. +**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. -**Expected Results:** Describe what you expected to see. +**Steps to Reproduce**: List the step by step process to reproduce the issue. -**Actual Results:** Describe what you actually saw. +**Expected Results**: Describe what you expected to see. -**Configuration:** What does the used config.js file look like? (Don't forget to remove any sensitive information.) +**Actual Results**: Describe what you actually saw. -**Additional Notes:** Provide any other relevant notes not previously mentioned (optional) +**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information! + +**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1ac9aaa1..215950e0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,7 @@ -> Please send your PR's the develop branch. -> Don't forget to add the change to changelog.md. +> 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? [yes | no] +* Does the pull request solve a **related** issue? * If so, can you reference the issue? -* What does the pull request accomplish? (please list) +* What does the pull request accomplish? Use a list if needed. * If it includes major visual changes please add screenshots. - - diff --git a/.stylelintrc b/.stylelintrc index 02300404..db05c713 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,4 +1,5 @@ { "extends": "stylelint-config-standard", - "font-family-name-quotes": "double-where-recommended" -} \ No newline at end of file + "font-family-name-quotes": "double-where-recommended", + "block-no-empty": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index df710e02..5c9e649a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,62 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## Unpublished +## [2.1.0] - Unreleased + +**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` + +### Added +- Finnish translation. +- Danish 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 minimumn 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. ### Updated -- Force fullscreen when kioskmode is active. +- Modified translations for Frysk. +- 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. + +### 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 + +## [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 the abilty to turn off and on the date display in the Clock Module + +### Fixed +- Fix typo in installer. +- Add message to unsupported Pi error to mention that Pi Zeros must use server only mode, as ARMv6 is unsupported. Closes #374. +- Fix API url for weather API. + +### Updated +- Force fullscreen when kioskmode is active. +- Update the .github templates and information with more modern information. +- Update the Gruntfile with a more functional StyleLint implementation. ## [2.0.4] - 2016-08-07 ### Added - Brazilian Portuguese Translation. -- Option to enable Kios mode. +- Option to enable Kiosk mode. - Added ability to start the app with Dev Tools. - Added ability to turn off the date display in `clock.js` when in analog mode. - Greek Translation @@ -76,4 +122,4 @@ It includes (but is not limited to) the following features: ## [1.0.0] - 2014-02-16 ### Initial release of MagicMirror. -This was part of the blogpost: http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the +This was part of the blogpost: [http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the) diff --git a/Gruntfile.js b/Gruntfile.js index ac499fe3..c47f55b9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,21 +8,59 @@ module.exports = function(grunt) { }, target: ["js/*.js", "modules/default/*.js", "serveronly/*.js", "*.js"] }, - postcss: { - lint: { + stylelint: { + simple: { options: { - processors: [ - require("stylelint")({"extends": "stylelint-config-standard", "font-family-name-quotes": "double-where-recommended"}), - require("postcss-reporter")({ clearMessages: true }) - ] + configFile: ".stylelintrc" }, - dist: { - src: "**/**/**/**/**/**/**/**.css" + src: ["css/main.css", "modules/default/calendar/calendar.css", "modules/default/clock/clock_styles.css", "modules/default/currentweather/currentweather.css", "modules/default/weatherforcast/weatherforcast.css"] + } + }, + jsonlint: { + main: { + src: ["package.json", ".eslintrc.json", ".stylelint"], + options: { + reporter: "jshint" } } + }, + markdownlint: { + all: { + options: { + config: { + "default": true, + "line-length": false, + "blanks-around-headers": false, + "no-duplicate-header": false, + "no-inline-html": false, + "MD010": false, + "MD001": false, + "MD031": false, + "MD040": false, + "MD002": false, + "MD029": false, + "MD041": false, + "MD032": false, + "MD036": false, + "MD037": false, + "MD009": false, + "MD018": false, + "MD012": false, + "MD026": false, + "MD038": false + } + }, + src: ["README.md", "CHANGELOG.md", "LICENSE.md", "modules/README.md", "modules/default/**/*.md", "!modules/default/calendar/vendor/ical.js/readme.md"] + } + }, + yamllint: { + all: [".travis.yml"] } }); grunt.loadNpmTasks("grunt-eslint"); - grunt.loadNpmTasks("grunt-postcss"); - grunt.registerTask("default", ["eslint", "postcss:lint"]); + grunt.loadNpmTasks("grunt-stylelint"); + grunt.loadNpmTasks("grunt-jsonlint"); + grunt.loadNpmTasks("grunt-yamllint"); + grunt.loadNpmTasks("grunt-markdownlint"); + grunt.registerTask("default", ["eslint", "stylelint", "jsonlint", "markdownlint", "yamllint"]); }; \ No newline at end of file diff --git a/README.md b/README.md index 5d5f7176..0f9ec65d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Dependency Status devDependency Status - Node Version + License Travis Known Vulnerabilities @@ -24,17 +24,17 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec ## Usage -#### Raspberry Pi Support +### Raspberry Pi Support Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2 & 3. The Raspberry Pi 1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself. -#### Automatic Installer (Raspberry Pi Only!) +### Automatic Installer (Raspberry Pi Only!) Execute the following command on your Raspberry Pi to install MagicMirror²: ```` curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh | bash ```` -#### Manual Installation +### Manual Installation 1. Download and install the latest Node.js version. 2. Clone the repository and check out the beta branch: `git clone https://github.com/MichMich/MagicMirror` @@ -45,23 +45,23 @@ curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installer **Note:** if you want to debug on Raspberry Pi you can use `npm start dev` which will start the MagicMirror app with Dev Tools enabled. -#### Server Only +### Server Only In some cases, you want to start the application without an actual app window. In this case, execute the following command from the MagicMirror folder: `node serveronly`. This will start the server, after which you can open the application in your browser of choice. -#### Raspberry Configuration & Auto Start. +### Raspberry Configuration & Auto Start. The following wiki links are helpful in the configuration of your MagicMirror² operating system: - [Configuring the Raspberry Pi](https://github.com/MichMich/MagicMirror/wiki/Configuring-the-Raspberry-Pi) - [Auto Starting MagicMirror](https://github.com/MichMich/MagicMirror/wiki/Auto-Starting-MagicMirror) -#### Updating you MagicMirror² +### Updating 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: -```` -git pull -```` +```bash +git pull && npm install +``` If you changed nothing more than the config or the modules, this should work without any problems. Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible. @@ -73,16 +73,17 @@ 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`. | +| `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"]]`).| | `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`| | `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.** | + Module configuration: | **Option** | **Description** | @@ -129,3 +130,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! + +

+
+ MagPi Top 50 +

diff --git a/config/config.js.sample b/config/config.js.sample index 04c7fba9..c2607dbf 100644 --- a/config/config.js.sample +++ b/config/config.js.sample @@ -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_center" + }, { module: 'clock', position: 'top_left' diff --git a/css/main.css b/css/main.css index 93d6a7ee..5e63e596 100644 --- a/css/main.css +++ b/css/main.css @@ -1,10 +1,10 @@ html { cursor: none; - overflow:hidden; + overflow: hidden; } -::-webkit-scrollbar { - display: none; +::-webkit-scrollbar { + display: none; } body { @@ -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; } /** diff --git a/index.html b/index.html index 4288eba1..86a69a4a 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,10 @@ + +
diff --git a/installers/raspberry.sh b/installers/raspberry.sh index 824bf7cc..4c42dcde 100644 --- a/installers/raspberry.sh +++ b/installers/raspberry.sh @@ -28,6 +28,7 @@ ARM=$(uname -m) if [ "$ARM" != "armv7l" ]; then echo -e "\e[91mSorry, your Raspberry Pi is not supported." echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3." + echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode." exit; fi @@ -68,7 +69,7 @@ else NODE_INSTALL=true fi -# Install or upgare node if nessecery. +# Install or upgrade node if necessary. if $NODE_INSTALL; then echo -e "\e[96mInstalling Node.js ...\e[90m" @@ -112,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 " " diff --git a/js/app.js b/js/app.js index 14f10e8b..84e89d13 100644 --- a/js/app.js +++ b/js/app.js @@ -10,6 +10,10 @@ 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); + // 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) { @@ -82,6 +86,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 +118,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. diff --git a/js/class.js b/js/class.js index b1793f16..01a92603 100644 --- a/js/class.js +++ b/js/class.js @@ -21,28 +21,31 @@ 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 +69,24 @@ }; })(); +//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;} diff --git a/js/defaults.js b/js/defaults.js index 0688595c..0f27e817 100644 --- a/js/defaults.js +++ b/js/defaults.js @@ -10,12 +10,17 @@ var defaults = { port: 8080, kioskmode: false, + ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], language: "en", timeFormat: 24, units: "metric", modules: [ + { + module: "updatenotification", + position: "top_center" + }, { module: "helloworld", position: "upper_third", diff --git a/js/loader.js b/js/loader.js index 3656c7cd..84e42a0a 100644 --- a/js/loader.js +++ b/js/loader.js @@ -117,9 +117,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) { diff --git a/js/logger.js b/js/logger.js index 5b2df990..280103ab 100644 --- a/js/logger.js +++ b/js/logger.js @@ -13,35 +13,35 @@ var Log = (function() { return { - info: function(message) { - console.info(message); + info: function() { + console.info.apply(console, arguments); }, - log: function(message) { - console.log(message); + log: function() { + console.log.apply(console, arguments); }, - error: function(message) { - console.error(message); + error: function() { + console.error.apply(console, arguments); }, - warn: function(message) { - console.warn(message); + warn: function() { + console.warn.apply(console, arguments); }, - group: function(message) { - console.group(message); + group: function() { + console.group.apply(console, arguments); }, - groupCollapsed: function(message) { - console.groupCollapsed(message); + groupCollapsed: function() { + console.groupCollapsed.apply(console, arguments); }, groupEnd: function() { console.groupEnd(); }, - time: function(message) { - console.time(message); + time: function() { + console.time.apply(console, arguments); }, - timeEnd: function(message) { - console.timeEnd(message); + timeEnd: function() { + console.timeEnd.apply(console, arguments); }, - timeStamp: function(message) { - console.timeStamp(message); + timeStamp: function() { + console.timeStamp.apply(console, arguments); } }; })(); diff --git a/js/main.js b/js/main.js index abd93e40..9c489ee0 100644 --- a/js/main.js +++ b/js/main.js @@ -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); } @@ -94,26 +95,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 +127,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.innerHTML; + } + + var tempContentWrapper = document.createElement("div"); + tempContentWrapper.appendChild(newContent); + contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML; + + return headerNeedsUpdate || contentNeedsUpdate; }; /* moduleNeedsUpdate(module, newContent) @@ -141,12 +152,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 +174,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 +193,10 @@ 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"; if (typeof callback === "function") { callback(); } }, speed); @@ -182,7 +210,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"; @@ -401,10 +452,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 +465,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); } }; diff --git a/js/module.js b/js/module.js index 293d00b5..4815895e 100644 --- a/js/module.js +++ b/js/module.js @@ -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 stings 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 - An array with filenames. */ - getScripts: function() { + getScripts: function () { return []; }, @@ -48,7 +55,7 @@ var Module = Class.extend({ * * return Array - An array with filenames. */ - getStyles: function() { + getStyles: function () { return []; }, @@ -57,7 +64,7 @@ var Module = Class.extend({ * * return Map - 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,6 +91,17 @@ 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. @@ -92,7 +110,7 @@ var Module = Class.extend({ * 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 { @@ -106,21 +124,21 @@ var Module = Class.extend({ * argument notification string - The identifier of the noitication. * 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,7 +165,7 @@ var Module = Class.extend({ * * argument config obejct - Module config. */ - setConfig: function(config) { + setConfig: function (config) { this.config = Object.assign(this.defaults, config); }, @@ -155,13 +173,13 @@ var Module = Class.extend({ * 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,7 +292,7 @@ var Module = Class.extend({ * * argument speed Number - The speed of the animation. (Optional) */ - updateDom: function(speed) { + updateDom: function (speed) { MM.updateDom(this, speed); }, @@ -284,7 +302,7 @@ var Module = Class.extend({ * argument notification string - The identifier of the noitication. * argument payload mixed - The payload of the notification. */ - sendNotification: function(notification, payload) { + sendNotification: function (notification, payload) { MM.sendNotification(notification, payload, this); }, @@ -294,7 +312,7 @@ var Module = Class.extend({ * argument notification string - The identifier of the noitication. * 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; }; diff --git a/js/server.js b/js/server.js index 2ab0b1b9..9ab47a7d 100644 --- a/js/server.js +++ b/js/server.js @@ -10,11 +10,24 @@ 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 Server = function(config, callback) { console.log("Starting server op port " + config.port + " ... "); server.listen(config.port); + + 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.
Please check your config.js or config.js.sample to change this."); + }); + }); + app.use("/js", express.static(__dirname)); app.use("/config", express.static(path.resolve(__dirname + "/../config"))); app.use("/css", express.static(path.resolve(__dirname + "/../css"))); @@ -23,8 +36,15 @@ var Server = function(config, callback) { app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor"))); app.use("/translations", express.static(path.resolve(__dirname + "/../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(__dirname + "/../index.html"), {encoding: "utf8"}); + html = html.replace("#VERSION#", global.version); + + res.send(html); }); if (typeof callback === "function") { diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..2b354edc --- /dev/null +++ b/jsconfig.json @@ -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" + ] +} diff --git a/modules/README.md b/modules/README.md index 39072db5..5f2713a6 100644 --- a/modules/README.md +++ b/modules/README.md @@ -4,13 +4,13 @@ This document describes the way to develop your own MagicMirror² modules. ## Module structure -All modules are loaded in de `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. +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 confiugure 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.
-**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.
+**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.
-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.
***payload* AnyType** - Optional. A notification payload.
-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.
***payload* AnyType** - Optional. A notification payload.
-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.
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.
**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.
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.
**Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender). +####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 moduleA’s 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 moduleA’s 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.
***payload* AnyType** - Optional. A notification payload.
-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.
-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.
-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 diff --git a/modules/default/calendar/README.md b/modules/default/calendar/README.md index 05d12de8..54d318ce 100644 --- a/modules/default/calendar/README.md +++ b/modules/default/calendar/README.md @@ -106,7 +106,6 @@ The following properties can be configured: titleReplace An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title.

Example:
- titleReplace: {'Birthday of ' : '', 'foo':'bar'} @@ -126,6 +125,13 @@ The following properties can be configured:
Default value: relative + + getRelative + How much time (in hours) should be left until calendar events start getting relative?
+
Possible values: 0 (events stay absolute) - 48 (48 hours before the event starts) +
Default value: 6 + + urgency When using a timeFormat of absolute, the urgency setting allows you to display events within a specific time frame as relative @@ -134,6 +140,13 @@ The following properties can be configured:
Default value: 0 (disabled) + + broadcastEvents + If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: CALENDAR_EVENTS. The event objects are stored in an array and contain the following fields: title, startDate, endDate, fullDayEvent, location and geo.
+
Possible values: true, false
+
Default value: true + + @@ -173,7 +186,7 @@ config: { symbol The symbol to show in front of an event. This property is optional.
-
Possible values: See Font Awsome website. +
Possible values: See Font Awesome website. @@ -183,5 +196,13 @@ config: { 'Birthday' + + user + The username for HTTP Basic authentication. + + + pass + The password for HTTP Basic authentication. + diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index c90153a0..39ea6b58 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -7,7 +7,7 @@ * MIT Licensed. */ -Module.register("calendar",{ +Module.register("calendar", { // Define module defaults defaults: { @@ -16,13 +16,14 @@ 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", + getRelative: 6, fadePoint: 0.25, // Start on 1/4th of the list. calendars: [ { @@ -34,20 +35,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 +57,7 @@ Module.register("calendar",{ }, // Override start method. - start: function() { + start: function () { Log.log("Starting module: " + this.name); // Set locale. @@ -64,7 +66,7 @@ Module.register("calendar",{ for (var c in this.config.calendars) { var calendar = this.config.calendars[c]; calendar.url = calendar.url.replace("webcal://", "http://"); - this.addCalendar(calendar.url); + this.addCalendar(calendar.url, calendar.user, calendar.pass); } this.calendarData = {}; @@ -72,11 +74,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 +96,7 @@ Module.register("calendar",{ }, // Override dom generator. - getDom: function() { + getDom: function () { var events = this.createEventList(); var wrapper = document.createElement("table"); @@ -109,27 +115,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 !== '') { + if (repeatingCountTitle !== "") { var thisYear = new Date().getFullYear(), yearDiff = thisYear - event.firstYear; - repeatingCountTitle = ', '+ yearDiff + '. ' + repeatingCountTitle; + repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; } } @@ -137,19 +143,25 @@ 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"); + 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 = this.capFirst(moment(event.startDate, "x").fromNow()); + } } else { /* Check to see if the user displays absolute or relative dates with their events * Also check to see if an event is happening within an 'urgency' time frameElement @@ -159,26 +171,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("MMM Do")); } } 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 @@ -189,18 +201,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("MMM Do")); } } 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'); @@ -234,7 +246,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) { @@ -250,7 +262,7 @@ 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) { @@ -263,7 +275,7 @@ Module.register("calendar",{ } } - events.sort(function(a, b) { + events.sort(function (a, b) { return a.startDate - b.startDate; }); @@ -275,12 +287,14 @@ Module.register("calendar",{ * * argument url sting - Url to add. */ - addCalendar: function(url) { + addCalendar: function (url, user, pass) { this.sendSocketNotification("ADD_CALENDAR", { url: url, maximumEntries: this.config.maximumEntries, maximumNumberOfDays: this.config.maximumNumberOfDays, - fetchInterval: this.config.fetchInterval + fetchInterval: this.config.fetchInterval, + user: user, + pass: pass }); }, @@ -291,10 +305,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; } } @@ -308,10 +322,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; } } @@ -328,14 +342,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) + "…"; + return string.slice(0, maxLength) + "…"; } 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. @@ -345,7 +368,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); @@ -353,5 +376,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); + } }); diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index f7f68b7e..7f142546 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -8,7 +8,7 @@ var ical = require("./vendor/ical.js"); var moment = require("moment"); -var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays) { +var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays, user, pass) { var self = this; var reloadTimer = null; @@ -27,9 +27,18 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe 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 6.0.0) MagicMirror/v2 (https://github.com/MichMich/MagicMirror/)" + } + }; + + if (user && pass) { + opts.auth = { + user: user, + pass: pass, + sendImmediately: true } } + ical.fromURL(url, opts, function(err, data) { if (err) { fetchFailedCallback(self, err); @@ -68,7 +77,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe if (!isFacebookBirthday) { endDate = startDate; } else { - endDate = moment(startDate).add(1, 'days'); + endDate = moment(startDate).add(1, "days"); } } @@ -92,7 +101,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe 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, @@ -123,12 +132,19 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe continue; } - // Every thing is good. Add it to the list. + + var location = event.location || false; + var geo = event.geo || false; + + // 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, + location: location, + geo: geo }); } @@ -239,4 +255,4 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe }; -module.exports = CalendarFetcher; \ No newline at end of file +module.exports = CalendarFetcher; diff --git a/modules/default/calendar/node_helper.js b/modules/default/calendar/node_helper.js index 47a0231b..a1b8db09 100644 --- a/modules/default/calendar/node_helper.js +++ b/modules/default/calendar/node_helper.js @@ -25,7 +25,7 @@ module.exports = NodeHelper.create({ socketNotificationReceived: function(notification, payload) { if (notification === "ADD_CALENDAR") { //console.log('ADD_CALENDAR: '); - this.createFetcher(payload.url, payload.fetchInterval, payload.maximumEntries, payload.maximumNumberOfDays); + this.createFetcher(payload.url, payload.fetchInterval, payload.maximumEntries, payload.maximumNumberOfDays, payload.user, payload.pass); } }, @@ -37,7 +37,7 @@ module.exports = NodeHelper.create({ * attribute reloadInterval number - Reload interval in milliseconds. */ - createFetcher: function(url, fetchInterval, maximumEntries, maximumNumberOfDays) { + createFetcher: function(url, fetchInterval, maximumEntries, maximumNumberOfDays, user, pass) { var self = this; if (!validUrl.isUri(url)) { @@ -48,7 +48,7 @@ module.exports = NodeHelper.create({ var fetcher; if (typeof self.fetchers[url] === "undefined") { console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); - fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays); + fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays, user, pass); fetcher.onReceive(function(fetcher) { //console.log('Broadcast events.'); diff --git a/modules/default/clock/README.md b/modules/default/clock/README.md index c7ec6bcb..6eca1efb 100644 --- a/modules/default/clock/README.md +++ b/modules/default/clock/README.md @@ -66,6 +66,13 @@ The following properties can be configured:
Default value: false + + showDate + Turn off or on the Date section.
+
Possible values: true or false +
Default value: true + + displayType Display a digital clock, analog clock, or both together.
@@ -109,4 +116,4 @@ The following properties can be configured: - + \ No newline at end of file diff --git a/modules/default/clock/clock.js b/modules/default/clock/clock.js index e3415375..db41d7e0 100644 --- a/modules/default/clock/clock.js +++ b/modules/default/clock/clock.js @@ -15,6 +15,7 @@ Module.register("clock",{ showPeriod: true, showPeriodUpper: false, clockBold: false, + showDate: true, /* specific to the analog clock */ analogSize: '200px', @@ -85,7 +86,9 @@ Module.register("clock",{ timeString = moment().format("h:mm"); } } + if(this.config.showDate){ dateWrapper.innerHTML = moment().format("dddd, LL"); + } timeWrapper.innerHTML = timeString; secondsWrapper.innerHTML = moment().format("ss"); if (this.config.showPeriodUpper) { @@ -224,4 +227,4 @@ Module.register("clock",{ // Return the wrapper to the dom. return wrapper; } -}); +}); \ No newline at end of file diff --git a/modules/default/clock/clock_styles.css b/modules/default/clock/clock_styles.css index 6020e579..1df9bf83 100644 --- a/modules/default/clock/clock_styles.css +++ b/modules/default/clock/clock_styles.css @@ -1,74 +1,68 @@ -#analog { -} - -#digital { -} - .clockCircle { - margin: 0 auto; - position: relative; - border-radius: 50%; - background-size: 100%; + margin: 0 auto; + position: relative; + border-radius: 50%; + background-size: 100%; } .clockFace { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } -.clockFace:after { - position: absolute; - top: 50%; - left: 50%; - width: 6px; - height: 6px; - margin: -3px 0 0 -3px; - background: white; - border-radius: 3px; - content: ""; - display: block; +.clockFace::after { + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + margin: -3px 0 0 -3px; + background: white; + border-radius: 3px; + content: ""; + display: block; } .clockHour { - width: 0; - height: 0; - position: absolute; - top: 50%; - left: 50%; - margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */ - padding: 2px 0 2px 25%; /* indicator length & thickness */ - background: white; - -webkit-transform-origin: 100% 50%; - -ms-transform-origin: 100% 50%; - transform-origin: 100% 50%; - border-radius: 3px 0 0 3px; + width: 0; + height: 0; + position: absolute; + top: 50%; + left: 50%; + margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */ + padding: 2px 0 2px 25%; /* indicator length & thickness */ + background: white; + -webkit-transform-origin: 100% 50%; + -ms-transform-origin: 100% 50%; + transform-origin: 100% 50%; + border-radius: 3px 0 0 3px; } .clockMinute { - width: 0; - height: 0; - position: absolute; - top: 50%; - left: 50%; - margin: -35% -2px 0; /* numbers must match negative length & thickness */ - padding: 35% 2px 0; /* indicator length & thickness */ - background: white; - -webkit-transform-origin: 50% 100%; - -ms-transform-origin: 50% 100%; - transform-origin: 50% 100%; - border-radius: 3px 0 0 3px; + width: 0; + height: 0; + position: absolute; + top: 50%; + left: 50%; + margin: -35% -2px 0; /* numbers must match negative length & thickness */ + padding: 35% 2px 0; /* indicator length & thickness */ + background: white; + -webkit-transform-origin: 50% 100%; + -ms-transform-origin: 50% 100%; + transform-origin: 50% 100%; + border-radius: 3px 0 0 3px; } .clockSecond { - width: 0; - height: 0; - position: absolute; - top: 50%; - left: 50%; - margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ - padding: 38% 1px 0 0; /* indicator length & thickness */ - background: #888888; - -webkit-transform-origin: 50% 100%; - -ms-transform-origin: 50% 100%; - transform-origin: 50% 100%; + width: 0; + height: 0; + position: absolute; + top: 50%; + left: 50%; + margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ + padding: 38% 1px 0 0; /* indicator length & thickness */ + background: #888; + -webkit-transform-origin: 50% 100%; + -ms-transform-origin: 50% 100%; + transform-origin: 50% 100%; } diff --git a/modules/default/currentweather/README.md b/modules/default/currentweather/README.md index 2a6f7844..067dce54 100644 --- a/modules/default/currentweather/README.md +++ b/modules/default/currentweather/README.md @@ -35,19 +35,20 @@ The following properties can be configured: - location The location used for weather information.
-
Example: Amsterdam,Netherlands -
Default value: New York +
Example: 'Amsterdam,Netherlands' +
Default value: New York

+ Note: When the location and locationID 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. locationID Location ID from OpenWeather This will override anything you put in location.
Leave blank if you want to use location.
Example: 1234567 -
Default value: +
Default value:

+ Note: When the location and locationID 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. @@ -106,6 +107,13 @@ The following properties can be configured: + showHumidity + Show the current humidity
+
Possible values: true or false +
Default value: false + + + useBeaufort Pick between using the Beaufort scale for wind speed or using the default units.

Possible values: true or false @@ -151,6 +159,18 @@ The following properties can be configured:
Default value: 'weather' + + appendLocationNameToHeader + If set to true, 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.
+
Default value: true + + + + calendarClass + The class for the calender module to base the event based weather information on.
+
Default value: 'calendar' + + iconTable The conversion table to convert the weather conditions to weather-icons.
diff --git a/modules/default/currentweather/currentweather.css b/modules/default/currentweather/currentweather.css index 9e9d9ed3..a40be878 100644 --- a/modules/default/currentweather/currentweather.css +++ b/modules/default/currentweather/currentweather.css @@ -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; +} diff --git a/modules/default/currentweather/currentweather.js b/modules/default/currentweather/currentweather.js index db3d5de2..39db3f09 100644 --- a/modules/default/currentweather/currentweather.js +++ b/modules/default/currentweather/currentweather.js @@ -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,9 @@ Module.register("currentweather",{ apiBase: "http://api.openweathermap.org/data/", weatherEndpoint: "weather", + appendLocationNameToHeader: true, + calendarClass: "calendar", + iconTable: { "01d": "wi-day-sunny", "02d": "wi-day-cloudy", @@ -53,6 +57,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"]; @@ -102,12 +112,6 @@ Module.register("currentweather",{ return wrapper; } - if (this.config.location === "") { - wrapper.innerHTML = "Please set the openweather location 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"; @@ -117,6 +121,7 @@ Module.register("currentweather",{ 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 +139,22 @@ Module.register("currentweather",{ spacer.innerHTML = " "; small.appendChild(spacer); + if (this.config.showHumidity) { + var humidity = document.createElement("span"); + humidity.innerHTML = this.humidity; + + var spacer = document.createElement("sup"); + spacer.innerHTML = " "; + + var humidityIcon = document.createElement("sup"); + humidityIcon.className = "wi wi-humidity humidityIcon"; + humidityIcon.innerHTML = " "; + + 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 +163,8 @@ Module.register("currentweather",{ sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime; small.appendChild(sunriseSunsetTime); + wrapper.appendChild(small); + var large = document.createElement("div"); large.className = "large light"; @@ -154,17 +177,55 @@ Module.register("currentweather",{ temperature.innerHTML = " " + this.temperature + "°"; 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() { - var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + '/' + this.getParams(); + 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 +236,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 +259,19 @@ Module.register("currentweather",{ */ getParams: function() { var params = "?"; - if(this.config.locationID !== "") { + if(this.config.locationID !== false) { params += "id=" + this.config.locationID; - } else { + } else if(this.config.location !== false) { 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 +292,7 @@ Module.register("currentweather",{ return; } + this.humidity = parseFloat(data.main.humidity); this.temperature = this.roundValue(data.main.temp); if (this.config.useBeaufort){ @@ -265,7 +334,7 @@ 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); }, diff --git a/modules/default/defaultmodules.js b/modules/default/defaultmodules.js index 42e89ee0..fccf3c52 100644 --- a/modules/default/defaultmodules.js +++ b/modules/default/defaultmodules.js @@ -15,7 +15,8 @@ var defaultModules = [ "currentweather", "helloworld", "newsfeed", - "weatherforecast" + "weatherforecast", + "updatenotification" ]; /*************** DO NOT EDIT THE LINE BELOW ***************/ diff --git a/modules/default/newsfeed/README.md b/modules/default/newsfeed/README.md index 882ef94a..951fa51c 100644 --- a/modules/default/newsfeed/README.md +++ b/modules/default/newsfeed/README.md @@ -98,7 +98,7 @@ The following properties can be configured: animationSpeed Speed of the update animation. (Milliseconds)

Possible values:0 - 5000 -
Default value: 2000 (2.5 seconds) +
Default value: 2000 (2 seconds) @@ -108,7 +108,37 @@ The following properties can be configured:
Default value: 0 - + removeStartTags: false, + removeEndTags: false, + startTags: [], + endTags: [] + + + + removeStartTags + Some newsfeeds feature tags at the beginning of their titles or descriptions, such as [VIDEO]. + This setting allows for the removal of specified tags from the beginning of an item's description and/or title.
+
Possible values:'title', 'description', 'both' + + + + startTags + List the tags you would like to have removed at the beginning of the feed item
+
Possible values: ['TAG'] or ['TAG1','TAG2',...] + + + + removeEndTags + Remove specified tags from the end of an item's description and/or title.
+
Possible values:'title', 'description', 'both' + + + + endTags + List the tags you would like to have removed at the end of the feed item
+
Possible values: ['TAG'] or ['TAG1','TAG2',...] + + diff --git a/modules/default/newsfeed/fetcher.js b/modules/default/newsfeed/fetcher.js index ff0a9980..665c5e15 100644 --- a/modules/default/newsfeed/fetcher.js +++ b/modules/default/newsfeed/fetcher.js @@ -60,11 +60,11 @@ var Fetcher = function(url, reloadInterval, encoding) { } 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); } }); diff --git a/modules/default/newsfeed/newsfeed.js b/modules/default/newsfeed/newsfeed.js index a9664085..7d8d08c7 100644 --- a/modules/default/newsfeed/newsfeed.js +++ b/modules/default/newsfeed/newsfeed.js @@ -24,7 +24,12 @@ Module.register("newsfeed",{ reloadInterval: 5 * 60 * 1000, // every 5 minutes updateInterval: 10 * 1000, animationSpeed: 2.5 * 1000, - maxNewsItems: 0 // 0 for unlimited + maxNewsItems: 0, // 0 for unlimited + removeStartTags: '', + removeEndTags: '', + startTags: [], + endTags: [] + }, // Define required scripts. @@ -96,11 +101,54 @@ Module.register("newsfeed",{ wrapper.appendChild(sourceAndTimestamp); } + //Remove selected tags from the beginning of rss feed items (title or description) + + if (this.config.removeStartTags == 'title' || 'both') { + + for (f=0; f + + + + Option + Description + + + + + updateInterval + How often do you want to check for a new version? This value represents the interval in milliseconds.
+
Possible values: Any value above 60000 (1 minute); +
Default value: 600000 (10 minutes); + + + + \ No newline at end of file diff --git a/modules/default/updatenotification/node_helper.js b/modules/default/updatenotification/node_helper.js new file mode 100644 index 00000000..862087d8 --- /dev/null +++ b/modules/default/updatenotification/node_helper.js @@ -0,0 +1,44 @@ +var simpleGit = require("simple-git")(__dirname + "/../.."); +var NodeHelper = require("node_helper"); + +module.exports = NodeHelper.create({ + + config: {}, + + updateTimer: null, + + start: function () { + + }, + + socketNotificationReceived: function (notification, payload) { + if (notification === "CONFIG") { + this.config = payload; + this.preformFetch(); + } + }, + + preformFetch() { + var self = this; + simpleGit.fetch().status(function(err, data) { + 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); + } + +}); diff --git a/modules/default/updatenotification/updatenotification.js b/modules/default/updatenotification/updatenotification.js new file mode 100644 index 00000000..cd2ba42c --- /dev/null +++ b/modules/default/updatenotification/updatenotification.js @@ -0,0 +1,67 @@ +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.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 = " "; + message.appendChild(icon); + + var text = document.createElement("span"); + text.innerHTML = this.translate("UPDATE_NOTIFICATION"); + 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; + } +}); diff --git a/modules/default/weatherforecast/README.md b/modules/default/weatherforecast/README.md index aba03a41..0d321aa3 100644 --- a/modules/default/weatherforecast/README.md +++ b/modules/default/weatherforecast/README.md @@ -35,19 +35,20 @@ The following properties can be configured: - location The location used for weather information.
-
Example: Amsterdam,Netherlands -
Default value: New York +
Example: 'Amsterdam,Netherlands' +
Default value: New York

+ Note: When the location and locationID 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. locationID Location ID from OpenWeather This will override anything you put in location.
Leave blank if you want to use location.
Example: 1234567 -
Default value: +
Default value:

+ Note: When the location and locationID 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. @@ -71,6 +72,14 @@ The following properties can be configured:
This value is optional. By default the weatherforecast module will return 7 days. + + showRainAmount + Should the predicted rain amount be displayed?
+
Possible values: true or false +
Default value: false +
This value is optional. By default the weatherforecast module will not display the predicted amount of rain. + + updateInterval How often does the content needs to be fetched? (Milliseconds)
@@ -85,7 +94,6 @@ The following properties can be configured:
Default value: 2000 (2 seconds) - lang The language of the days.
@@ -139,6 +147,18 @@ The following properties can be configured:
Default value: 'forecast/daily' + + appendLocationNameToHeader + If set to true, 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.
+
Default value: true + + + + calendarClass + The class for the calender module to base the event based weather information on.
+
Default value: 'calendar' + + iconTable The conversion table to convert the weather conditions to weather-icons.
@@ -164,6 +184,5 @@ The following properties can be configured: } - diff --git a/modules/default/weatherforecast/weatherforecast.css b/modules/default/weatherforecast/weatherforecast.css index 7b12c42d..62c9767f 100644 --- a/modules/default/weatherforecast/weatherforecast.css +++ b/modules/default/weatherforecast/weatherforecast.css @@ -12,3 +12,8 @@ padding-left: 20px; padding-right: 0; } + +.weatherforecast .rain { + padding-left: 20px; + padding-right: 0; +} diff --git a/modules/default/weatherforecast/weatherforecast.js b/modules/default/weatherforecast/weatherforecast.js index 706da252..7f3ef71d 100644 --- a/modules/default/weatherforecast/weatherforecast.js +++ b/modules/default/weatherforecast/weatherforecast.js @@ -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,9 @@ Module.register("weatherforecast",{ apiBase: "http://api.openweathermap.org/data/", forecastEndpoint: "forecast/daily", + appendLocationNameToHeader: true, + calendarClass: "calendar", + iconTable: { "01d": "wi-day-sunny", "02d": "wi-day-cloudy", @@ -52,6 +56,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,14 +105,8 @@ Module.register("weatherforecast",{ return wrapper; } - if (this.config.location === "") { - wrapper.innerHTML = "Please set the openweather location in the config for module: " + this.name + "."; - wrapper.className = "dimmed light small"; - return wrapper; - } - if (!this.loaded) { - wrapper.innerHTML = this.translate('LOADING'); + wrapper.innerHTML = this.translate("LOADING"); wrapper.className = "dimmed light small"; return wrapper; } @@ -139,6 +143,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,12 +171,51 @@ 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() { - var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + '/' + this.getParams(); + 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 +226,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 +249,19 @@ Module.register("weatherforecast",{ */ getParams: function() { var params = "?"; - if(this.config.locationID !== "") { + if(this.config.locationID !== false) { params += "id=" + this.config.locationID; - } else { + } else if(this.config.location !== false) { 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 +281,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 +292,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); }, diff --git a/package.json b/package.json index 8c2b226b..4d7944e6 100644 --- a/package.json +++ b/package.json @@ -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", - "jscs": "jscs **/**/**/**/*.js", - "stylelint": "stylelint css/main.css fonts/roboto.css", - "htmlvalidator": "html-validator --file=index.html", "test": "snyk test", "snyk-protect": "snyk protect", "prepublish": "npm run snyk-protect" @@ -30,26 +27,29 @@ }, "homepage": "https://github.com/MichMich/MagicMirror#readme", "devDependencies": { - "electron-prebuilt": "latest", "grunt": "latest", "grunt-eslint": "latest", - "grunt-postcss": "latest", - "postcss-reporter": "latest", - "stylelint": "latest", + "grunt-jsonlint": "latest", + "grunt-markdownlint": "^1.0.4", + "grunt-stylelint": "latest", + "grunt-yamllint": "latest", "stylelint-config-standard": "latest", "time-grunt": "latest" }, "dependencies": { + "electron-prebuilt": "^0.37.2", "express": "^4.14.0", + "express-ipfilter": "latest", "feedme": "latest", "iconv-lite": "latest", "moment": "latest", "request": "^2.74.0", "rrule": "latest", + "simple-git": "^1.54.0", "snyk": "^1.14.1", "socket.io": "^1.4.6", "valid-url": "latest", "walk": "latest" }, "snyk": true -} \ No newline at end of file +} diff --git a/splashscreen/MagicMirror.plymouth b/splashscreen/MagicMirror.plymouth new file mode 100644 index 00000000..b6887bf5 --- /dev/null +++ b/splashscreen/MagicMirror.plymouth @@ -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 diff --git a/splashscreen/MagicMirror.script b/splashscreen/MagicMirror.script new file mode 100644 index 00000000..d7e7b860 --- /dev/null +++ b/splashscreen/MagicMirror.script @@ -0,0 +1,50 @@ +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; + +flag = 1; + +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); diff --git a/splashscreen/splash.png b/splashscreen/splash.png new file mode 100644 index 00000000..f3da394b Binary files /dev/null and b/splashscreen/splash.png differ diff --git a/translations/da.json b/translations/da.json new file mode 100644 index 00000000..06827ea1 --- /dev/null +++ b/translations/da.json @@ -0,0 +1,28 @@ +{ + /* GENERAL */ + "LOADING": "Indlæser …", + + /* CALENDAR */ + "TODAY": "I dag", + "TOMORROW": "I morgen", + "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" +} diff --git a/translations/de.json b/translations/de.json index d4c20d10..58a4ca14 100644 --- a/translations/de.json +++ b/translations/de.json @@ -5,6 +5,7 @@ /* CALENDAR */ "TODAY": "Heute", "TOMORROW": "Morgen", + "DAYAFTERTOMORROW": "Übermorgen", "RUNNING": "noch", "EMPTY": "Keine Termine.", @@ -24,5 +25,9 @@ "W": "W", "WNW": "WNW", "NW": "NW", - "NNW": "NNW" + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.", + "UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch." } diff --git a/translations/en.json b/translations/en.json index a2d5933a..e169ba1b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -24,5 +24,9 @@ "W": "W", "WNW": "WNW", "NW": "NW", - "NNW": "NNW" + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² update available.", + "UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch." } diff --git a/translations/fi.json b/translations/fi.json new file mode 100644 index 00000000..f370e41c --- /dev/null +++ b/translations/fi.json @@ -0,0 +1,28 @@ +{ + /* GENERAL */ + "LOADING": "Lataa …", + + /* 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" +} diff --git a/translations/fy.json b/translations/fy.json index af6bcde8..770ae3a1 100644 --- a/translations/fy.json +++ b/translations/fy.json @@ -5,6 +5,7 @@ /* CALENDAR */ "TODAY": "Hjoed", "TOMORROW": "Moarn", + "DAYAFTERTOMORROW": "Oaremoarn", "RUNNING": "Einigest oer", "EMPTY": "Gjin plande ôfspraken.", diff --git a/translations/nl.json b/translations/nl.json index ab84807b..582b62e6 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -5,7 +5,8 @@ /* CALENDAR */ "TODAY": "Vandaag", "TOMORROW": "Morgen", - "RUNNING": "Eindigd over", + "DAYAFTERTOMORROW": "Overmorgen", + "RUNNING": "Eindigt over", "EMPTY": "Geen geplande afspraken.", /* WEATHER */ @@ -24,5 +25,9 @@ "W": "W", "WNW": "WNW", "NW": "NW", - "NNW": "NNW" + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.", + "UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch." } diff --git a/translations/translations.js b/translations/translations.js index 2a458ef2..81cf25a6 100644 --- a/translations/translations.js +++ b/translations/translations.js @@ -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,5 @@ var translations = { "ja" : "translations/ja.json", // Japanese "pl" : "translations/pl.json", // Polish "gr" : "translations/gr.json", // Greek + "da" : "translations/da.json", // Danish };