mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
Merge branch 'develop' into currentweather-module-updates
This commit is contained in:
commit
b9b9773df9
@ -12,5 +12,11 @@
|
|||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"globalReturn": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "8"
|
|
||||||
- "7"
|
- "7"
|
||||||
- "6"
|
|
||||||
- "5.1"
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- yarn danger ci
|
||||||
- npm install grunt-cli -g
|
- npm install grunt-cli -g
|
||||||
- "export DISPLAY=:99.0"
|
- "export DISPLAY=:99.0"
|
||||||
- "sh -e /etc/init.d/xvfb start"
|
- "sh -e /etc/init.d/xvfb start"
|
||||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@ -2,16 +2,42 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [2.3.0] - Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add new settings in compliments module: setting time intervals for morning and afternoon
|
||||||
|
- Add system notification `MODULE_DOM_CREATED` for notifying each module when their Dom has been fully loaded.
|
||||||
|
- Add types for module.
|
||||||
|
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||||
|
- Allow to scroll in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||||
|
- Changed 'compliments.js' - update DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||||
|
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||||
|
- Automated integration tests translations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Add link to GitHub repository which contains the respective Dockerfile.
|
||||||
|
- Optimized automated unit tests cloneObject, cmpVersions
|
||||||
|
- Update notifications use now translation templates instead of normal strings.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- News article in fullscreen (iframe) is now shown in front of modules.
|
||||||
|
|
||||||
|
*This release is scheduled to be released on 2018-04-01.*
|
||||||
|
|
||||||
## [2.2.2] - 2018-01-02
|
## [2.2.2] - 2018-01-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add missing `package-lock.json`.
|
- Add missing `package-lock.json`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed Electron dependency to v1.7.10.
|
||||||
|
|
||||||
## [2.2.1] - 2018-01-01
|
## [2.2.1] - 2018-01-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed linting errors.
|
- Fixed linting errors.
|
||||||
|
|
||||||
## [2.2.0] - 2018-01-01
|
## [2.2.0] - 2018-01-01
|
||||||
|
161
README.md
161
README.md
@ -16,45 +16,83 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
|
|||||||
|
|
||||||
## Table Of Contents
|
## Table Of Contents
|
||||||
|
|
||||||
- [Usage](#usage)
|
- [Installation](#installation)
|
||||||
|
- [Raspberry Pi](#raspberrypi)
|
||||||
|
- [General](#general)
|
||||||
|
- [Server Only](#server-only)
|
||||||
|
- [Client Only](#client-only)
|
||||||
|
- [Docker](#docker)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Modules](#modules)
|
- [Modules](#modules)
|
||||||
|
- [Updating](#updating)
|
||||||
- [Known Issues](#known-issues)
|
- [Known Issues](#known-issues)
|
||||||
- [Community](#community)
|
- [Community](#community)
|
||||||
- [Contributing Guidelines](#contributing-guidelines)
|
- [Contributing Guidelines](#contributing-guidelines)
|
||||||
|
- [Manifesto](#manifesto)
|
||||||
|
|
||||||
## Usage
|
## Installation
|
||||||
|
|
||||||
### Raspberry Pi Support
|
### Raspberry Pi
|
||||||
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 Installation (Raspberry Pi only!)
|
||||||
|
|
||||||
|
*Electron*, the app wrapper around MagicMirror², only supports the Raspberry Pi 2/3. The Raspberry Pi 0/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. (Yes, people have managed to run MM² also on a Pi0, so if you insist, search in the forums.)
|
||||||
|
|
||||||
|
Note that you will need to install the lastest full version of Raspbian, **don't use the Lite version**.
|
||||||
|
|
||||||
Execute the following command on your Raspberry Pi to install MagicMirror²:
|
Execute the following command on your Raspberry Pi to install MagicMirror²:
|
||||||
````
|
|
||||||
|
```bash
|
||||||
bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh)"
|
bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh)"
|
||||||
````
|
```
|
||||||
|
|
||||||
### Manual Installation
|
#### Manual Installation
|
||||||
|
|
||||||
1. Download and install the latest Node.js version.
|
1. Download and install the latest *Node.js* version.
|
||||||
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
||||||
3. Enter the repository: `cd ~/MagicMirror`
|
3. Enter the repository: `cd ~/MagicMirror`
|
||||||
4. Install and run the app: `npm install && npm start`
|
4. Install and run the app with: `npm install && npm start` \
|
||||||
|
For **Server Only** use: `npm install && node serveronly` .
|
||||||
|
|
||||||
**Important:** `npm start` does **not** work via SSH, use `DISPLAY=:0 nohup npm start &` instead. This starts the mirror on the remote display.
|
|
||||||
|
|
||||||
**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.
|
**:warning: Important!**
|
||||||
|
|
||||||
|
- **The installation step for `npm install` will take a very long time**, often with little or no terminal response! \
|
||||||
|
For the RPi3 this is **~10** minutes and for the Rpi2 **~25** minutes. \
|
||||||
|
Do not interrupt or you risk getting a :broken_heart: by Raspberry Jam.
|
||||||
|
|
||||||
|
|
||||||
|
Also note that:
|
||||||
|
|
||||||
|
- `npm start` does **not** work via SSH. But you can use `DISPLAY=:0 nohup npm start &` instead. \
|
||||||
|
This starts the mirror on the remote display.
|
||||||
|
- If you want to debug on Raspberry Pi you can use `npm start dev` which will start MM with *Dev Tools* enabled.
|
||||||
|
- To access toolbar menu when in mirror mode, hit `ALT` key.
|
||||||
|
- To toggle the (web) `Developer Tools` from mirror mode, use `CTRL-SHIFT-I` or `ALT` and select `View`.
|
||||||
|
|
||||||
|
|
||||||
### Server Only
|
### Server Only
|
||||||
|
|
||||||
In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below.
|
In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below.
|
||||||
|
|
||||||
|
**Important:** Make sure that you whitelist the interface/ip (`ipWhitelist`) in the server config where you want the client to connect to, otherwise it will not be allowed to connect to the server. You also need to set the local host `address` field to `0.0.0.0` in order for the RPi to listen on all interfaces and not only `localhost` (default).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var config = {
|
||||||
|
address: "0.0.0.0", // default is "localhost"
|
||||||
|
port: 8080, // default
|
||||||
|
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:172.17.0.1"], // default -- need to add your IP here
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Client Only
|
### Client Only
|
||||||
When you have a server running remotely and want to connect a standalone client to this instance, you can manually run `node clientonly --address 192.168.1.5 --port 8080`. (Specify the ip address and port number of the server)
|
|
||||||
|
|
||||||
**Important:** Make sure that you whitelist the interface/ip in the server config where you want the client to connect to, otherwise it will not be allowed to connect to the server
|
This is when you already have a server running remotely and want your RPi to connect as a standalone client to this instance, to show the MM from the server. Then from your RPi, you run it with: `node clientonly --address 192.168.1.5 --port 8080`. (Specify the ip address and port number of the server)
|
||||||
|
|
||||||
#### Docker
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell:
|
MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell:
|
||||||
|
|
||||||
@ -67,63 +105,34 @@ docker run -d \
|
|||||||
--name magic_mirror \
|
--name magic_mirror \
|
||||||
bastilimbach/docker-magicmirror
|
bastilimbach/docker-magicmirror
|
||||||
```
|
```
|
||||||
|
To get more information about the available Dockerfile versions and configurations head over to the respective [GitHub repository](https://github.com/bastilimbach/docker-MagicMirror).
|
||||||
|
|
||||||
| **Volumes** | **Description** |
|
|
||||||
| --- | --- |
|
|
||||||
| `/opt/magic_mirror/config` | Mount this volume to insert your own config into the docker container. |
|
|
||||||
| `/opt/magic_mirror/modules` | Mount this volume to add your own custom modules into the docker container. |
|
|
||||||
|
|
||||||
You may need to add your Docker Host IP to your `ipWhitelist` option. If you have some issues setting up this configuration, check [this forum post](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto).
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var config = {
|
|
||||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:172.17.0.1"]
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to run the server on a raspberry pi, use the `raspberry` tag. (bastilimbach/docker-magicmirror:raspberry)
|
|
||||||
|
|
||||||
#### Manual
|
|
||||||
|
|
||||||
1. Download and install the latest Node.js version.
|
|
||||||
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
|
|
||||||
3. Enter the repository: `cd ~/MagicMirror`
|
|
||||||
4. Install and run the app: `npm install && node serveronly`
|
|
||||||
|
|
||||||
### 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 your MagicMirror²
|
|
||||||
|
|
||||||
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull && 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.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
1. Duplicate `config/config.js.sample` to `config/config.js`. **Note:** If you used the installer script. This step is already done for you.
|
### Raspberry Specific
|
||||||
2. Modify your required settings.
|
|
||||||
|
The following wiki links are helpful for the initial 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)
|
||||||
|
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
1. Copy `config/config.js.sample` to `config/config.js`. \
|
||||||
|
**Note:** If you used the installer script. This step is already done for you.
|
||||||
|
|
||||||
|
2. Modify your required settings. \
|
||||||
|
Note: You'll can check your configuration running `npm run config:check`.
|
||||||
|
|
||||||
Note: You'll can check your configuration running the follow command:
|
|
||||||
```bash
|
|
||||||
npm run config:check
|
|
||||||
```
|
|
||||||
|
|
||||||
The following properties can be configured:
|
The following properties can be configured:
|
||||||
|
|
||||||
| **Option** | **Description** |
|
| **Option** | **Description** |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
|
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
|
||||||
| `address` | The ip address the accept connections. The default open bind `localhost`. Example config: `192.168.10.100`. |
|
| `address` | The *interface* ip address on which to accept connections. The default is `localhost`, which would prevent exposing the built-in webserver to machines on the local network. To expose it to other machines, use: `0.0.0.0`. |
|
||||||
| `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"]]`). Set `[]` to allow all IP addresses. For more information about how configure this directive see the [follow post ipWhitelist HowTo](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto) |
|
| `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"]`, which is from `localhost` only. Add your IP when needed. You can also specify IP ranges with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or directly with (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`). Set `[]` to allow all IP addresses. For more information see: [follow post ipWhitelist HowTo](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto) |
|
||||||
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
|
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
|
||||||
| `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. |
|
| `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`. |
|
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
|
||||||
@ -156,7 +165,19 @@ The following modules are installed by default.
|
|||||||
- [**Hello World**](modules/default/helloworld)
|
- [**Hello World**](modules/default/helloworld)
|
||||||
- [**Alert**](modules/default/alert)
|
- [**Alert**](modules/default/alert)
|
||||||
|
|
||||||
For more available modules, check out out the wiki page: [MagicMirror² Modules](https://github.com/MichMich/MagicMirror/wiki/MagicMirror²-Modules). If you want to build your own modules, check out the [MagicMirror² Module Development Documentation](modules) and don't forget to add it to the wiki and the [forum](https://forum.magicmirror.builders/category/7/showcase)!
|
For more available modules, check out out the wiki page [MagicMirror² 3rd Party Modules](https://github.com/MichMich/MagicMirror/wiki/3rd-party-modules). If you want to build your own modules, check out the [MagicMirror² Module Development Documentation](modules) and don't forget to add it to the wiki and the [forum](https://forum.magicmirror.builders/category/7/showcase)!
|
||||||
|
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull && 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.
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
@ -180,6 +201,22 @@ Please keep the following in mind:
|
|||||||
|
|
||||||
Thanks for your help in making MagicMirror² better!
|
Thanks for your help in making MagicMirror² better!
|
||||||
|
|
||||||
|
## Manifesto
|
||||||
|
|
||||||
|
A real Manifesto is still to be written. Till then, Michael's response on [one of the repository issues](https://github.com/MichMich/MagicMirror/issues/1174) gives a great summary:
|
||||||
|
|
||||||
|
> "... I started this project as an ultimate starter project for Raspberry Pi enthusiasts. As a matter of fact, for most of the contributors, the MagicMirror project is the first open source project they ever contributed to. This is one of the reasons why the MagicMirror project is featured in several RasPi magazines.
|
||||||
|
>
|
||||||
|
>The project has a lot of opportunities for improvement. We could use a powerful framework like Vue to ramp up the development speed. We could use SASS for better/easier css implementations. We could make it an NPM installable package. And as you say, we could bundle it up. The big downside of of of these changes is that it over complicates things: a user no longer will be able to open just one file and make a small modification and see how it works out.
|
||||||
|
>
|
||||||
|
>Of course, a bundled version can be complimentary to the regular un-bundled version. And I'm sure a lot of (new) users will opt for the bundled version. But this means those users won't be motivated to take a peek under the hood. They will just remain 'users'. They won't become contributors, and worse: they won't be motivated to take their first steps in software development.
|
||||||
|
>
|
||||||
|
>And to be honest: motivating curious users to step out of their comfort zone and take those first steps is what drives me in this project. Therefor my ultimate goal is this project is to keep it as accessible as possible."
|
||||||
|
>
|
||||||
|
> ~ Michael Teeuw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<br>
|
<br>
|
||||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||||
|
17
dangerfile.js
Normal file
17
dangerfile.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { danger, fail, warn } from "danger"
|
||||||
|
|
||||||
|
// Check if the CHANGELOG.md file has been edited
|
||||||
|
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||||
|
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||||
|
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the PR request is send to the master branch.
|
||||||
|
// This should only be done by MichMich.
|
||||||
|
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||||
|
// Check if the PR body or title includes the text: #accepted.
|
||||||
|
// If not, the PR will fail.
|
||||||
|
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||||
|
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.")
|
||||||
|
}
|
||||||
|
}
|
@ -92,7 +92,4 @@ function cloneObject(obj) {
|
|||||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
module.exports = Class;
|
module.exports = Class;
|
||||||
module.exports._test = {
|
|
||||||
cloneObject: cloneObject
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
71
js/main.js
71
js/main.js
@ -1,5 +1,5 @@
|
|||||||
/* global Log, Loader, Module, config, defaults */
|
/* global Log, Loader, Module, config, defaults */
|
||||||
/* jshint -W020 */
|
/* jshint -W020, esversion: 6 */
|
||||||
|
|
||||||
/* Magic Mirror
|
/* Magic Mirror
|
||||||
* Main System
|
* Main System
|
||||||
@ -19,10 +19,12 @@ var MM = (function() {
|
|||||||
* are configured for a specific position.
|
* are configured for a specific position.
|
||||||
*/
|
*/
|
||||||
var createDomObjects = function() {
|
var createDomObjects = function() {
|
||||||
for (var m in modules) {
|
var domCreationPromises = [];
|
||||||
var module = modules[m];
|
|
||||||
|
|
||||||
if (typeof module.data.position === "string") {
|
modules.forEach(module => {
|
||||||
|
if (typeof module.data.position !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var wrapper = selectWrapper(module.data.position);
|
var wrapper = selectWrapper(module.data.position);
|
||||||
|
|
||||||
@ -48,13 +50,18 @@ var MM = (function() {
|
|||||||
moduleContent.className = "module-content";
|
moduleContent.className = "module-content";
|
||||||
dom.appendChild(moduleContent);
|
dom.appendChild(moduleContent);
|
||||||
|
|
||||||
updateDom(module, 0);
|
var domCreationPromise = updateDom(module, 0);
|
||||||
}
|
domCreationPromises.push(domCreationPromise);
|
||||||
}
|
domCreationPromise.then(() => {
|
||||||
|
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
||||||
|
}).catch(Log.error);
|
||||||
|
});
|
||||||
|
|
||||||
updateWrapperStates();
|
updateWrapperStates();
|
||||||
|
|
||||||
|
Promise.all(domCreationPromises).then(() => {
|
||||||
sendNotification("DOM_OBJECTS_CREATED");
|
sendNotification("DOM_OBJECTS_CREATED");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/* selectWrapper(position)
|
/* selectWrapper(position)
|
||||||
@ -79,11 +86,12 @@ var MM = (function() {
|
|||||||
* argument notification string - The identifier of the notification.
|
* argument notification string - The identifier of the notification.
|
||||||
* argument payload mixed - The payload of the notification.
|
* argument payload mixed - The payload of the notification.
|
||||||
* argument sender Module - The module that sent the notification.
|
* argument sender Module - The module that sent the notification.
|
||||||
|
* argument sendTo Module - The module to send the notification to. (optional)
|
||||||
*/
|
*/
|
||||||
var sendNotification = function(notification, payload, sender) {
|
var sendNotification = function(notification, payload, sender, sendTo) {
|
||||||
for (var m in modules) {
|
for (var m in modules) {
|
||||||
var module = modules[m];
|
var module = modules[m];
|
||||||
if (module !== sender) {
|
if (module !== sender && (!sendTo || module === sendTo)) {
|
||||||
module.notificationReceived(notification, payload, sender);
|
module.notificationReceived(notification, payload, sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,19 +102,53 @@ var MM = (function() {
|
|||||||
*
|
*
|
||||||
* argument module Module - The module that needs an update.
|
* argument module Module - The module that needs an update.
|
||||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||||
|
*
|
||||||
|
* return Promise - Resolved when the dom is fully updated.
|
||||||
*/
|
*/
|
||||||
var updateDom = function(module, speed) {
|
var updateDom = function(module, speed) {
|
||||||
var newContent = module.getDom();
|
return new Promise((resolve) => {
|
||||||
|
var newContentPromise = module.getDom();
|
||||||
var newHeader = module.getHeader();
|
var newHeader = module.getHeader();
|
||||||
|
|
||||||
if (!module.hidden) {
|
if (!(newContentPromise instanceof Promise)) {
|
||||||
|
// convert to a promise if not already one to avoid if/else's everywhere
|
||||||
|
newContentPromise = Promise.resolve(newContentPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
newContentPromise.then((newContent) => {
|
||||||
|
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||||
|
|
||||||
|
updatePromise.then(resolve).catch(Log.error);
|
||||||
|
}).catch(Log.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* updateDomWithContent(module, speed, newHeader, newContent)
|
||||||
|
* Update the dom with the specified content
|
||||||
|
*
|
||||||
|
* argument module Module - The module that needs an update.
|
||||||
|
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
|
* argument newContent Domobject - The new content that is generated.
|
||||||
|
*
|
||||||
|
* return Promise - Resolved when the module dom has been updated.
|
||||||
|
*/
|
||||||
|
var updateDomWithContent = function(module, speed, newHeader, newContent) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (module.hidden || !speed) {
|
||||||
|
updateModuleContent(module, newHeader, newContent);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
|
if (!moduleNeedsUpdate(module, newHeader, newContent)) {
|
||||||
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!speed) {
|
if (!speed) {
|
||||||
updateModuleContent(module, newHeader, newContent);
|
updateModuleContent(module, newHeader, newContent);
|
||||||
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,16 +157,16 @@ var MM = (function() {
|
|||||||
if (!module.hidden) {
|
if (!module.hidden) {
|
||||||
showModule(module, speed / 2);
|
showModule(module, speed / 2);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
updateModuleContent(module, newHeader, newContent);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* moduleNeedsUpdate(module, newContent)
|
/* moduleNeedsUpdate(module, newContent)
|
||||||
* Check if the content has changed.
|
* Check if the content has changed.
|
||||||
*
|
*
|
||||||
* argument module Module - The module to check.
|
* argument module Module - The module to check.
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
* argument newContent Domobject - The new content that is generated.
|
* argument newContent Domobject - The new content that is generated.
|
||||||
*
|
*
|
||||||
* return bool - Does the module need an update?
|
* return bool - Does the module need an update?
|
||||||
@ -152,6 +194,7 @@ var MM = (function() {
|
|||||||
* Update the content of a module on screen.
|
* Update the content of a module on screen.
|
||||||
*
|
*
|
||||||
* argument module Module - The module to check.
|
* argument module Module - The module to check.
|
||||||
|
* argument newHeader String - The new header that is generated.
|
||||||
* argument newContent Domobject - The new content that is generated.
|
* argument newContent Domobject - The new content that is generated.
|
||||||
*/
|
*/
|
||||||
var updateModuleContent = function(module, newHeader, newContent) {
|
var updateModuleContent = function(module, newHeader, newContent) {
|
||||||
|
23
js/module.js
23
js/module.js
@ -78,9 +78,10 @@ var Module = Class.extend({
|
|||||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||||
* Alternatively, the getTemplete method could be subclassed.
|
* Alternatively, the getTemplete method could be subclassed.
|
||||||
*
|
*
|
||||||
* return domobject - The dom to display.
|
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||||
*/
|
*/
|
||||||
getDom: function () {
|
getDom: function () {
|
||||||
|
return new Promise((resolve) => {
|
||||||
var div = document.createElement("div");
|
var div = document.createElement("div");
|
||||||
var template = this.getTemplate();
|
var template = this.getTemplate();
|
||||||
var templateData = this.getTemplateData();
|
var templateData = this.getTemplateData();
|
||||||
@ -93,19 +94,17 @@ var Module = Class.extend({
|
|||||||
Log.error(err)
|
Log.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The inner content of the div will be set after the template is received.
|
|
||||||
// This isn't the most optimal way, but since it's near instant
|
|
||||||
// it probably won't be an issue.
|
|
||||||
// If it gives problems, we can always add a way to pre fetch the templates.
|
|
||||||
// Let's not over optimise this ... KISS! :)
|
|
||||||
div.innerHTML = res;
|
div.innerHTML = res;
|
||||||
|
|
||||||
|
resolve(div);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// the template is a template string.
|
// the template is a template string.
|
||||||
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
|
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
|
||||||
}
|
|
||||||
|
|
||||||
return div;
|
resolve(div);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* getHeader()
|
/* getHeader()
|
||||||
@ -477,11 +476,3 @@ Module.register = function (name, moduleDefinition) {
|
|||||||
Log.log("Module registered: " + name);
|
Log.log("Module registered: " + name);
|
||||||
Module.definitions[name] = moduleDefinition;
|
Module.definitions[name] = moduleDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof exports != "undefined") { // For testing purpose only
|
|
||||||
// A good a idea move the function cmpversions a helper file.
|
|
||||||
// It's used into other side.
|
|
||||||
exports._test = {
|
|
||||||
cmpVersions: cmpVersions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -156,11 +156,12 @@ var Translator = (function() {
|
|||||||
|
|
||||||
return key;
|
return key;
|
||||||
},
|
},
|
||||||
/* load(module, file, callback)
|
/* load(module, file, isFallback, callback)
|
||||||
* Load a translation file (json) and remember the data.
|
* Load a translation file (json) and remember the data.
|
||||||
*
|
*
|
||||||
* argument module Module - The module to load the translation file for.
|
* argument module Module - The module to load the translation file for.
|
||||||
* argument file string - Path of the file we want to load.
|
* argument file string - Path of the file we want to load.
|
||||||
|
* argument isFallback boolean - Flag to indicate fallback translations.
|
||||||
* argument callback function - Function called when done.
|
* argument callback function - Function called when done.
|
||||||
*/
|
*/
|
||||||
load: function(module, file, isFallback, callback) {
|
load: function(module, file, isFallback, callback) {
|
||||||
@ -216,10 +217,12 @@ var Translator = (function() {
|
|||||||
// defined translation after the following line.
|
// defined translation after the following line.
|
||||||
for (var first in translations) {break;}
|
for (var first in translations) {break;}
|
||||||
|
|
||||||
|
if (first) {
|
||||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||||
loadJSON(translations[first], function(translations) {
|
loadJSON(translations[first], function(translations) {
|
||||||
self.coreTranslationsFallback = translations;
|
self.coreTranslationsFallback = translations;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
31
module-types.ts
Normal file
31
module-types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
type ModuleProperties = {
|
||||||
|
defaults?: object,
|
||||||
|
start?(): void,
|
||||||
|
getHeader?(): string,
|
||||||
|
getTemplate?(): string,
|
||||||
|
getTemplateData?(): object,
|
||||||
|
notificationReceived?(notification: string, payload: any, sender: object): void,
|
||||||
|
socketNotificationReceived?(notification: string, payload: any): void,
|
||||||
|
suspend?(): void,
|
||||||
|
resume?(): void,
|
||||||
|
getDom?(): HTMLElement,
|
||||||
|
getStyles?(): string[],
|
||||||
|
[key: string]: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const Module: {
|
||||||
|
register(moduleName: string, moduleProperties: ModuleProperties): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const Log: {
|
||||||
|
info(message?: any, ...optionalParams: any[]): void,
|
||||||
|
log(message?: any, ...optionalParams: any[]): void,
|
||||||
|
error(message?: any, ...optionalParams: any[]): void,
|
||||||
|
warn(message?: any, ...optionalParams: any[]): void,
|
||||||
|
group(groupTitle?: string, ...optionalParams: any[]): void,
|
||||||
|
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
|
||||||
|
groupEnd(): void,
|
||||||
|
time(timerName?: string): void,
|
||||||
|
timeEnd(timerName?: string): void,
|
||||||
|
timeStamp(timerName?: string): void,
|
||||||
|
};
|
@ -2,6 +2,42 @@
|
|||||||
|
|
||||||
This document describes the way to develop your own MagicMirror² modules.
|
This document describes the way to develop your own MagicMirror² modules.
|
||||||
|
|
||||||
|
Table of Contents:
|
||||||
|
|
||||||
|
- Module structure
|
||||||
|
- Files
|
||||||
|
|
||||||
|
- The Core module file: modulename.js
|
||||||
|
- Available module instance properties
|
||||||
|
- Subclassable module methods
|
||||||
|
- Module instance methods
|
||||||
|
- Visibility locking
|
||||||
|
|
||||||
|
- The Node Helper: node_helper.js
|
||||||
|
- Available module instance properties
|
||||||
|
- Subclassable module methods
|
||||||
|
- Module instance methods
|
||||||
|
|
||||||
|
- MagicMirror Helper Methods
|
||||||
|
- Module Selection
|
||||||
|
|
||||||
|
- MagicMirror Logger
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## General Advice
|
||||||
|
|
||||||
|
As MagicMirror has gained huge popularity, so has the number of available modules. For new users and developers alike, it is very time consuming to navigate around the various repositories in order to find out what exactly a certain modules does, how it looks and what it depends on. Unfortunately, this information is rarely available, nor easily obtained without having to install it first.
|
||||||
|
Therefore **we highly recommend you to include the following information in your README file.**
|
||||||
|
|
||||||
|
- A high quality screenshot of your working module
|
||||||
|
- A short, one sentence, clear description what it does (duh!)
|
||||||
|
- What external API's it depend on, including web links to those
|
||||||
|
- Wheteher the API/request require a key and the user limitations of those. (Is it free?)
|
||||||
|
|
||||||
|
Surely this also help you get better recognition and feedback for your work.
|
||||||
|
|
||||||
## Module structure
|
## Module structure
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@ -14,7 +50,7 @@ A module can be placed in one single folder. Or multiple modules can be grouped
|
|||||||
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
## Core module file: modulename.js
|
## The Core module file: modulename.js
|
||||||
This is the script in which the module will be defined. This script is required in order for the module to be used. In it's most simple form, the core module file must contain:
|
This is the script in which the module will be defined. This script is required in order for the module to be used. In it's most simple form, the core module file must contain:
|
||||||
````javascript
|
````javascript
|
||||||
Module.register("modulename",{});
|
Module.register("modulename",{});
|
||||||
@ -44,30 +80,16 @@ As you can see, the `Module.register()` method takes two arguments: the name of
|
|||||||
### Available module instance properties
|
### Available module instance properties
|
||||||
After the module is initialized, the module instance has a few available module properties:
|
After the module is initialized, the module instance has a few available module properties:
|
||||||
|
|
||||||
#### `this.name`
|
| Instance Property | Type | Description |
|
||||||
**String**
|
|:----------------- |:---- |:----------- |
|
||||||
|
| `this.name` | String | The name of the module. |
|
||||||
|
| `this.identifier` | String | This is a unique identifier for the module instance. |
|
||||||
|
| `this.hidden` | Boolean | This represents if the module is currently hidden (faded away). |
|
||||||
|
| `this.config` | Boolean | The configuration of the module instance as set in the user's `config.js` file. This config will also contain the module's defaults if these properties are not over-written by the user config. |
|
||||||
|
| `this.data` | Object | The data object contain additional metadata about the module instance. (See below) |
|
||||||
|
|
||||||
The name of the module.
|
|
||||||
|
|
||||||
#### `this.identifier`
|
The `this.data` data object contain the follwoing metadata:
|
||||||
**String**
|
|
||||||
|
|
||||||
This is a unique identifier for the module instance.
|
|
||||||
|
|
||||||
#### `this.hidden`
|
|
||||||
**Boolean**
|
|
||||||
|
|
||||||
This represents if the module is currently hidden (faded away).
|
|
||||||
|
|
||||||
#### `this.config`
|
|
||||||
**Boolean**
|
|
||||||
|
|
||||||
The configuration of the module instance as set in the user's config.js file. This config will also contain the module's defaults if these properties are not over written by the user config.
|
|
||||||
|
|
||||||
#### `this.data`
|
|
||||||
**Object**
|
|
||||||
|
|
||||||
The data object contains additional metadata about the module instance:
|
|
||||||
- `data.classes` - The classes which are added to the module dom wrapper.
|
- `data.classes` - The classes which are added to the module dom wrapper.
|
||||||
- `data.file` - The filename of the core module file.
|
- `data.file` - The filename of the core module file.
|
||||||
- `data.path` - The path of the module folder.
|
- `data.path` - The path of the module folder.
|
||||||
@ -230,11 +252,12 @@ notificationReceived: function(notification, payload, sender) {
|
|||||||
}
|
}
|
||||||
````
|
````
|
||||||
|
|
||||||
**Note:** the system sends two notifications when starting up. These notifications could come in handy!
|
**Note:** the system sends three 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.
|
- `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules.
|
||||||
- `DOM_OBJECTS_CREATED` - All dom objects are created. The system is now ready to perform visual changes.
|
- `DOM_OBJECTS_CREATED` - All dom objects are created. The system is now ready to perform visual changes.
|
||||||
|
- `MODULE_DOM_CREATED` - This module's dom has been fully loaded. You can now access your module's dom objects.
|
||||||
|
|
||||||
|
|
||||||
#### `socketNotificationReceived: function(notification, payload)`
|
#### `socketNotificationReceived: function(notification, payload)`
|
||||||
|
BIN
modules/default/.DS_Store
vendored
Normal file
BIN
modules/default/.DS_Store
vendored
Normal file
Binary file not shown.
@ -108,7 +108,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
assert.equal(topic.end.getFullYear(), 1998);
|
assert.equal(topic.end.getFullYear(), 1998);
|
||||||
assert.equal(topic.end.getUTCMonth(), 2);
|
assert.equal(topic.end.getUTCMonth(), 2);
|
||||||
assert.equal(topic.end.getUTCDate(), 15);
|
assert.equal(topic.end.getUTCDate(), 15);
|
||||||
assert.equal(topic.end.getUTCHours(), 00);
|
assert.equal(topic.end.getUTCHours(), 0);
|
||||||
assert.equal(topic.end.getUTCMinutes(), 30);
|
assert.equal(topic.end.getUTCMinutes(), 30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,7 +146,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
}
|
}
|
||||||
, 'has a start datetime' : function(topic) {
|
, 'has a start datetime' : function(topic) {
|
||||||
assert.equal(topic.start.getFullYear(), 2011);
|
assert.equal(topic.start.getFullYear(), 2011);
|
||||||
assert.equal(topic.start.getMonth(), 09);
|
assert.equal(topic.start.getMonth(), 9);
|
||||||
assert.equal(topic.start.getDate(), 11);
|
assert.equal(topic.start.getDate(), 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
}
|
}
|
||||||
, 'has a start' : function(topic){
|
, 'has a start' : function(topic){
|
||||||
assert.equal(topic.start.tz, 'America/Phoenix')
|
assert.equal(topic.start.tz, 'America/Phoenix')
|
||||||
assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString())
|
assert.equal(topic.start.toISOString(), new Date(2011, 10, 9, 19, 0,0).toISOString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,7 +208,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
, 'has a start' : function(topic){
|
, 'has a start' : function(topic){
|
||||||
assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString())
|
assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 12, 0,0).toISOString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
, 'event with rrule' :{
|
, 'event with rrule' :{
|
||||||
@ -249,7 +249,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
},
|
},
|
||||||
'task completed': function(task){
|
'task completed': function(task){
|
||||||
assert.equal(task.completion, 100);
|
assert.equal(task.completion, 100);
|
||||||
assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString());
|
assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,7 +367,7 @@ vows.describe('node-ical').addBatch({
|
|||||||
assert.equal(topic.end.getFullYear(), 2014);
|
assert.equal(topic.end.getFullYear(), 2014);
|
||||||
assert.equal(topic.end.getMonth(), 3);
|
assert.equal(topic.end.getMonth(), 3);
|
||||||
assert.equal(topic.end.getUTCHours(), 19);
|
assert.equal(topic.end.getUTCHours(), 19);
|
||||||
assert.equal(topic.end.getUTCMinutes(), 00);
|
assert.equal(topic.end.getUTCMinutes(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,12 @@ The following properties can be configured:
|
|||||||
| `compliments` | The list of compliments. <br><br> **Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below. <br> **Default value:** See _compliment configuration_ below.
|
| `compliments` | The list of compliments. <br><br> **Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below. <br> **Default value:** See _compliment configuration_ below.
|
||||||
| `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json` <br> **Default value:** `null` (Do not load from file)
|
| `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json` <br> **Default value:** `null` (Do not load from file)
|
||||||
| `classes` | Override the CSS classes of the div showing the compliments <br><br> **Default value:** `thin xlarge bright`
|
| `classes` | Override the CSS classes of the div showing the compliments <br><br> **Default value:** `thin xlarge bright`
|
||||||
|
| `morningStartTime` | Time in hours (in 24 format), after which the mode of "morning" will begin <br> **Possible values:** `0` - `24` <br><br> **Default value:** `3`
|
||||||
|
| `morningEndTime` | Time in hours (in 24 format), after which the mode of "morning" will end <br> **Possible values:** `0` - `24` <br><br> **Default value:** `12`
|
||||||
|
| `afternoonStartTime` | Time in hours (in 24 format), after which the mode "afternoon" will begin <br> **Possible values:** `0` - `24` <br><br> **Default value:** `12`
|
||||||
|
| `afternoonEndTime` | Time in hours (in 24 format), after which the mode "afternoon" will end <br> **Possible values:** `0` - `24` <br><br> **Default value:** `17`
|
||||||
|
|
||||||
|
All the rest of the time that does not fall into the morningStartTime-morningEndTime and afternoonStartTime-afternoonEndTime ranges is considered "evening".
|
||||||
|
|
||||||
### Compliment configuration
|
### Compliment configuration
|
||||||
|
|
||||||
|
@ -32,7 +32,11 @@ Module.register("compliments", {
|
|||||||
},
|
},
|
||||||
updateInterval: 30000,
|
updateInterval: 30000,
|
||||||
remoteFile: null,
|
remoteFile: null,
|
||||||
fadeSpeed: 4000
|
fadeSpeed: 4000,
|
||||||
|
morningStartTime: 3,
|
||||||
|
morningEndTime: 12,
|
||||||
|
afternoonStartTime: 12,
|
||||||
|
afternoonEndTime: 17
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set currentweather from module
|
// Set currentweather from module
|
||||||
@ -49,14 +53,15 @@ Module.register("compliments", {
|
|||||||
|
|
||||||
this.lastComplimentIndex = -1;
|
this.lastComplimentIndex = -1;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
if (this.config.remoteFile != null) {
|
if (this.config.remoteFile != null) {
|
||||||
this.complimentFile((response) => {
|
this.complimentFile((response) => {
|
||||||
this.config.compliments = JSON.parse(response);
|
this.config.compliments = JSON.parse(response);
|
||||||
|
self.updateDom();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule update timer.
|
// Schedule update timer.
|
||||||
var self = this;
|
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
self.updateDom(self.config.fadeSpeed);
|
self.updateDom(self.config.fadeSpeed);
|
||||||
}, this.config.updateInterval);
|
}, this.config.updateInterval);
|
||||||
@ -98,9 +103,9 @@ Module.register("compliments", {
|
|||||||
var hour = moment().hour();
|
var hour = moment().hour();
|
||||||
var compliments;
|
var compliments;
|
||||||
|
|
||||||
if (hour >= 3 && hour < 12 && this.config.compliments.hasOwnProperty("morning")) {
|
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||||
compliments = this.config.compliments.morning.slice(0);
|
compliments = this.config.compliments.morning.slice(0);
|
||||||
} else if (hour >= 12 && hour < 17 && this.config.compliments.hasOwnProperty("afternoon")) {
|
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||||
compliments = this.config.compliments.afternoon.slice(0);
|
compliments = this.config.compliments.afternoon.slice(0);
|
||||||
} else if(this.config.compliments.hasOwnProperty("evening")) {
|
} else if(this.config.compliments.hasOwnProperty("evening")) {
|
||||||
compliments = this.config.compliments.evening.slice(0);
|
compliments = this.config.compliments.evening.slice(0);
|
||||||
|
@ -43,7 +43,7 @@ The following properties can be configured:
|
|||||||
| `showWindDirectionAsArrow` | Show the wind direction as an arrow instead of abbreviation <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `showWindDirectionAsArrow` | Show the wind direction as an arrow instead of abbreviation <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `showHumidity` | Show the current humidity <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `showHumidity` | Show the current humidity <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `showIndoorTemperature` | If you have another module that emits the INDOOR_TEMPERATURE notification, the indoor temperature will be displayed <br> **Default value:** `false`
|
| `showIndoorTemperature` | If you have another module that emits the INDOOR_TEMPERATURE notification, the indoor temperature will be displayed <br> **Default value:** `false`
|
||||||
| `onlyTemp` | Show only current Temperature and weather icon. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
| `onlyTemp` | Show only current Temperature and weather icon without windspeed, sunset and sunrise time. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||||
| `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
| `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||||
| `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_
|
| `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_
|
||||||
| `decimalSymbol` | The decimal symbol to use.<br><br> **Possible values:** `.`, `,` or any other symbol.<br> **Default value:** `.`
|
| `decimalSymbol` | The decimal symbol to use.<br><br> **Possible values:** `.`, `,` or any other symbol.<br> **Default value:** `.`
|
||||||
|
@ -39,7 +39,7 @@ MagicMirror's [notification mechanism](https://github.com/MichMich/MagicMirror/t
|
|||||||
| ----------------------- | -----------
|
| ----------------------- | -----------
|
||||||
| `ARTICLE_NEXT` | Shows the next news title (hiding the summary or previously fully displayed article)
|
| `ARTICLE_NEXT` | Shows the next news title (hiding the summary or previously fully displayed article)
|
||||||
| `ARTICLE_PREVIOUS` | Shows the previous news title (hiding the summary or previously fully displayed article)
|
| `ARTICLE_PREVIOUS` | Shows the previous news title (hiding the summary or previously fully displayed article)
|
||||||
| `ARTICLE_MORE_DETAILS` | When received the _first time_, shows the corresponding description of the currently displayed news title. <br> The module expects that the module's configuration option `showDescription` is set to `false` (default value). <br><br> When received a _second consecutive time_, shows the full news article in an IFRAME. <br> This requires that the news page can be embedded in an IFRAME, e.g. doesn't have the HTTP response header [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) set to e.g. `DENY`.
|
| `ARTICLE_MORE_DETAILS` | When received the _first time_, shows the corresponding description of the currently displayed news title. <br> The module expects that the module's configuration option `showDescription` is set to `false` (default value). <br><br> When received a _second consecutive time_, shows the full news article in an IFRAME. <br> This requires that the news page can be embedded in an IFRAME, e.g. doesn't have the HTTP response header [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) set to e.g. `DENY`.<br><br>When received the _next consecutive times_, reloads the page and scrolls down by `scrollLength` pixels to paginate through the article.
|
||||||
| `ARTICLE_LESS_DETAILS` | Hides the summary or full news article and only displays the news title of the currently viewed news item.
|
| `ARTICLE_LESS_DETAILS` | Hides the summary or full news article and only displays the news title of the currently viewed news item.
|
||||||
|
|
||||||
Note the payload of the sent notification event is ignored.
|
Note the payload of the sent notification event is ignored.
|
||||||
@ -72,13 +72,14 @@ The following properties can be configured:
|
|||||||
| `updateInterval` | How often do you want to display a new headline? (Milliseconds) <br><br> **Possible values:**`1000` - `60000` <br> **Default value:** `10000` (10 seconds)
|
| `updateInterval` | How often do you want to display a new headline? (Milliseconds) <br><br> **Possible values:**`1000` - `60000` <br> **Default value:** `10000` (10 seconds)
|
||||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `2500` (2.5 seconds)
|
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `2500` (2.5 seconds)
|
||||||
| `maxNewsItems` | Total amount of news items to cycle through. (0 for unlimited) <br><br> **Possible values:**`0` - `...` <br> **Default value:** `0`
|
| `maxNewsItems` | Total amount of news items to cycle through. (0 for unlimited) <br><br> **Possible values:**`0` - `...` <br> **Default value:** `0`
|
||||||
| `ignoreOldItems` | Ignore news items that are outdated. <br><br> **Possible values:**`true` or `false <br> **Default value:** `false`
|
| `ignoreOldItems` | Ignore news items that are outdated. <br><br> **Possible values:**`true` or `false` <br> **Default value:** `false`
|
||||||
| `ignoreOlderThan` | How old should news items be before they are considered outdated? (Milliseconds) <br><br> **Possible values:**`1` - `...` <br> **Default value:** `86400000` (1 day)
|
| `ignoreOlderThan` | How old should news items be before they are considered outdated? (Milliseconds) <br><br> **Possible values:**`1` - `...` <br> **Default value:** `86400000` (1 day)
|
||||||
| `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. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
| `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. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
||||||
| `startTags` | List the tags you would like to have removed at the beginning of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
| `startTags` | List the tags you would like to have removed at the beginning of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
||||||
| `removeEndTags` | Remove specified tags from the **end** of an item's description and/or title. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
| `removeEndTags` | Remove specified tags from the **end** of an item's description and/or title. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
||||||
| `endTags` | List the tags you would like to have removed at the end of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
| `endTags` | List the tags you would like to have removed at the end of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
||||||
| `prohibitedWords` | Remove news feed item if one of these words is found anywhere in the title (case insensitive and greedy matching) <br><br> **Possible values:** `['word']` or `['word1','word2',...]`
|
| `prohibitedWords` | Remove news feed item if one of these words is found anywhere in the title (case insensitive and greedy matching) <br><br> **Possible values:** `['word']` or `['word1','word2',...]`
|
||||||
|
| `scrollLength` | Scrolls the full news article page by a given number of pixels when a `ARTICLE_MORE_DETAILS` notification is received and the full news article is already displayed.<br><br> **Possible values:** `1` or `10000` <br> **Default value:** `500`
|
||||||
|
|
||||||
The `feeds` property contains an array with multiple objects. These objects have the following properties:
|
The `feeds` property contains an array with multiple objects. These objects have the following properties:
|
||||||
|
|
||||||
|
@ -36,7 +36,8 @@ Module.register("newsfeed",{
|
|||||||
removeEndTags: "",
|
removeEndTags: "",
|
||||||
startTags: [],
|
startTags: [],
|
||||||
endTags: [],
|
endTags: [],
|
||||||
prohibitedWords: []
|
prohibitedWords: [],
|
||||||
|
scrollLength: 500
|
||||||
},
|
},
|
||||||
|
|
||||||
// Define required scripts.
|
// Define required scripts.
|
||||||
@ -62,6 +63,7 @@ Module.register("newsfeed",{
|
|||||||
this.newsItems = [];
|
this.newsItems = [];
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.activeItem = 0;
|
this.activeItem = 0;
|
||||||
|
this.scrollPosition = 0;
|
||||||
|
|
||||||
this.registerFeeds();
|
this.registerFeeds();
|
||||||
|
|
||||||
@ -171,7 +173,6 @@ Module.register("newsfeed",{
|
|||||||
var description = document.createElement("div");
|
var description = document.createElement("div");
|
||||||
description.className = "small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
description.className = "small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||||
var txtDesc = this.newsItems[this.activeItem].description;
|
var txtDesc = this.newsItems[this.activeItem].description;
|
||||||
//Log.info('txtDesc.length = ' + txtDesc.length + " - " + this.config.lengthDescription);
|
|
||||||
description.innerHTML = (this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc);
|
description.innerHTML = (this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc);
|
||||||
wrapper.appendChild(description);
|
wrapper.appendChild(description);
|
||||||
}
|
}
|
||||||
@ -180,12 +181,14 @@ Module.register("newsfeed",{
|
|||||||
var fullArticle = document.createElement("iframe");
|
var fullArticle = document.createElement("iframe");
|
||||||
fullArticle.className = "";
|
fullArticle.className = "";
|
||||||
fullArticle.style.width = "100%";
|
fullArticle.style.width = "100%";
|
||||||
|
// very large height value to allow scrolling
|
||||||
|
fullArticle.height = "10000";
|
||||||
|
fullArticle.style.height = "10000";
|
||||||
fullArticle.style.top = "0";
|
fullArticle.style.top = "0";
|
||||||
fullArticle.style.left = "0";
|
fullArticle.style.left = "0";
|
||||||
fullArticle.style.position = "fixed";
|
|
||||||
fullArticle.height = window.innerHeight;
|
|
||||||
fullArticle.style.border = "none";
|
fullArticle.style.border = "none";
|
||||||
fullArticle.src = this.newsItems[this.activeItem].url;
|
fullArticle.src = typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||||
|
fullArticle.style.zIndex = 1;
|
||||||
wrapper.appendChild(fullArticle);
|
wrapper.appendChild(fullArticle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,6 +325,10 @@ Module.register("newsfeed",{
|
|||||||
resetDescrOrFullArticleAndTimer: function() {
|
resetDescrOrFullArticleAndTimer: function() {
|
||||||
this.config.showDescription = false;
|
this.config.showDescription = false;
|
||||||
this.config.showFullArticle = false;
|
this.config.showFullArticle = false;
|
||||||
|
this.scrollPosition = 0;
|
||||||
|
// reset bottom bar alignment
|
||||||
|
document.getElementsByClassName("region bottom bar")[0].style.bottom = "0";
|
||||||
|
document.getElementsByClassName("region bottom bar")[0].style.top = "inherit";
|
||||||
if(!timer){
|
if(!timer){
|
||||||
this.scheduleUpdateInterval();
|
this.scheduleUpdateInterval();
|
||||||
}
|
}
|
||||||
@ -350,12 +357,27 @@ Module.register("newsfeed",{
|
|||||||
}
|
}
|
||||||
// if "more details" is received the first time: show article summary, on second time show full article
|
// if "more details" is received the first time: show article summary, on second time show full article
|
||||||
else if(notification == "ARTICLE_MORE_DETAILS"){
|
else if(notification == "ARTICLE_MORE_DETAILS"){
|
||||||
|
// full article is already showing, so scrolling down
|
||||||
|
if(this.config.showFullArticle == true){
|
||||||
|
this.scrollPosition += this.config.scrollLength;
|
||||||
|
window.scrollTo(0, this.scrollPosition);
|
||||||
|
Log.info(this.name + " - scrolling down");
|
||||||
|
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||||
|
}
|
||||||
|
// display full article
|
||||||
|
else {
|
||||||
this.config.showDescription = !this.config.showDescription;
|
this.config.showDescription = !this.config.showDescription;
|
||||||
this.config.showFullArticle = !this.config.showDescription;
|
this.config.showFullArticle = !this.config.showDescription;
|
||||||
|
// make bottom bar align to top to allow scrolling
|
||||||
|
if(this.config.showFullArticle == true){
|
||||||
|
document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit";
|
||||||
|
document.getElementsByClassName("region bottom bar")[0].style.top = "-90px";
|
||||||
|
}
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
Log.info(this.name + " - showing " + this.config.showDescription ? "article description" : "full article");
|
Log.info(this.name + " - showing " + this.config.showDescription ? "article description" : "full article");
|
||||||
this.updateDom(100);
|
this.updateDom(100);
|
||||||
|
}
|
||||||
} else if(notification == "ARTICLE_LESS_DETAILS"){
|
} else if(notification == "ARTICLE_LESS_DETAILS"){
|
||||||
this.resetDescrOrFullArticleAndTimer();
|
this.resetDescrOrFullArticleAndTimer();
|
||||||
Log.info(this.name + " - showing only article titles again");
|
Log.info(this.name + " - showing only article titles again");
|
||||||
|
@ -58,16 +58,19 @@ Module.register("updatenotification", {
|
|||||||
icon.innerHTML = " ";
|
icon.innerHTML = " ";
|
||||||
message.appendChild(icon);
|
message.appendChild(icon);
|
||||||
|
|
||||||
var subtextHtml = this.translate("UPDATE_INFO")
|
var subtextHtml = this.translate("UPDATE_INFO", {
|
||||||
.replace("COMMIT_COUNT", this.status.behind + " " + ((this.status.behind == 1) ? "commit" : "commits"))
|
COMMIT_COUNT: this.status.behind + " " + ((this.status.behind == 1) ? "commit" : "commits"),
|
||||||
.replace("BRANCH_NAME", this.status.current);
|
BRANCH_NAME: this.status.current
|
||||||
|
});
|
||||||
|
|
||||||
var text = document.createElement("span");
|
var text = document.createElement("span");
|
||||||
if (this.status.module == "default") {
|
if (this.status.module == "default") {
|
||||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||||
subtextHtml = this.diffLink(subtextHtml);
|
subtextHtml = this.diffLink(subtextHtml);
|
||||||
} else {
|
} else {
|
||||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE").replace("MODULE_NAME", this.status.module);
|
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
|
||||||
|
MODULE_NAME: this.status.module
|
||||||
|
});
|
||||||
}
|
}
|
||||||
message.appendChild(text);
|
message.appendChild(text);
|
||||||
|
|
||||||
|
1113
package-lock.json
generated
1113
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "magicmirror",
|
"name": "magicmirror",
|
||||||
"version": "2.2.2",
|
"version": "2.3.0-dev",
|
||||||
"description": "The open source modular smart mirror platform.",
|
"description": "The open source modular smart mirror platform.",
|
||||||
"main": "js/electron.js",
|
"main": "js/electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -11,7 +11,8 @@
|
|||||||
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive",
|
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive",
|
||||||
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive",
|
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive",
|
||||||
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive",
|
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive",
|
||||||
"config:check": "node tests/configs/check_config.js"
|
"config:check": "node tests/configs/check_config.js",
|
||||||
|
"lint": "grunt"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -36,6 +37,7 @@
|
|||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"current-week-number": "^1.0.7",
|
"current-week-number": "^1.0.7",
|
||||||
|
"danger": "^3.1.3",
|
||||||
"grunt": "latest",
|
"grunt": "latest",
|
||||||
"grunt-eslint": "latest",
|
"grunt-eslint": "latest",
|
||||||
"grunt-jsonlint": "latest",
|
"grunt-jsonlint": "latest",
|
||||||
@ -43,6 +45,7 @@
|
|||||||
"grunt-stylelint": "latest",
|
"grunt-stylelint": "latest",
|
||||||
"grunt-yamllint": "latest",
|
"grunt-yamllint": "latest",
|
||||||
"http-auth": "^3.2.3",
|
"http-auth": "^3.2.3",
|
||||||
|
"jsdom": "^11.6.2",
|
||||||
"jshint": "^2.9.5",
|
"jshint": "^2.9.5",
|
||||||
"mocha": "^4.1.0",
|
"mocha": "^4.1.0",
|
||||||
"mocha-each": "^1.1.0",
|
"mocha-each": "^1.1.0",
|
||||||
@ -54,7 +57,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.18.2",
|
"body-parser": "^1.18.2",
|
||||||
"colors": "^1.1.2",
|
"colors": "^1.1.2",
|
||||||
"electron": "1.4.15",
|
"electron": "^1.7.10",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"express-ipfilter": "0.3.1",
|
"express-ipfilter": "0.3.1",
|
||||||
"feedme": "latest",
|
"feedme": "latest",
|
||||||
|
@ -14,8 +14,6 @@ var path = require("path");
|
|||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var Utils = require(__dirname + "/../../js/utils.js");
|
var Utils = require(__dirname + "/../../js/utils.js");
|
||||||
|
|
||||||
if (process.env.NODE_ENV == "test") { return 0 };
|
|
||||||
|
|
||||||
/* getConfigFile()
|
/* getConfigFile()
|
||||||
* Return string with path of configuration file
|
* Return string with path of configuration file
|
||||||
* Check if set by enviroment variable MM_CONFIG_FILE
|
* Check if set by enviroment variable MM_CONFIG_FILE
|
||||||
@ -30,6 +28,7 @@ function getConfigFile() {
|
|||||||
return configFileName;
|
return configFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkConfigFile() {
|
||||||
var configFileName = getConfigFile();
|
var configFileName = getConfigFile();
|
||||||
// Check if file is present
|
// Check if file is present
|
||||||
if (fs.existsSync(configFileName) === false) {
|
if (fs.existsSync(configFileName) === false) {
|
||||||
@ -64,3 +63,8 @@ fs.readFile(configFileName, "utf-8", function (err, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "test") {
|
||||||
|
checkConfigFile();
|
||||||
|
};
|
13
tests/configs/data/StripComments.json
Normal file
13
tests/configs/data/StripComments.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// Escaped
|
||||||
|
"FOO\"BAR": "Today",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following lines
|
||||||
|
* represent cardinal directions
|
||||||
|
*/
|
||||||
|
"N": "N",
|
||||||
|
"E": "E",
|
||||||
|
"S": "S",
|
||||||
|
"W": "W"
|
||||||
|
}
|
32
tests/configs/data/TranslationTest.json
Normal file
32
tests/configs/data/TranslationTest.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "Loading …",
|
||||||
|
|
||||||
|
"TODAY": "Today",
|
||||||
|
"TOMORROW": "Tomorrow",
|
||||||
|
"DAYAFTERTOMORROW": "In 2 days",
|
||||||
|
"RUNNING": "Ends in",
|
||||||
|
"EMPTY": "No upcoming events.",
|
||||||
|
|
||||||
|
"WEEK": "Week {weekNumber}",
|
||||||
|
|
||||||
|
"N": "N",
|
||||||
|
"NNE": "NNE",
|
||||||
|
"NE": "NE",
|
||||||
|
"ENE": "ENE",
|
||||||
|
"E": "E",
|
||||||
|
"ESE": "ESE",
|
||||||
|
"SE": "SE",
|
||||||
|
"SSE": "SSE",
|
||||||
|
"S": "S",
|
||||||
|
"SSW": "SSW",
|
||||||
|
"SW": "SW",
|
||||||
|
"WSW": "WSW",
|
||||||
|
"W": "W",
|
||||||
|
"WNW": "WNW",
|
||||||
|
"NW": "NW",
|
||||||
|
"NNW": "NNW",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
||||||
|
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
|
||||||
|
}
|
32
tests/configs/data/en.json
Normal file
32
tests/configs/data/en.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "Loading …",
|
||||||
|
|
||||||
|
"TODAY": "Today",
|
||||||
|
"TOMORROW": "Tomorrow",
|
||||||
|
"DAYAFTERTOMORROW": "In 2 days",
|
||||||
|
"RUNNING": "Ends in",
|
||||||
|
"EMPTY": "No upcoming events.",
|
||||||
|
|
||||||
|
"WEEK": "Week {weekNumber}",
|
||||||
|
|
||||||
|
"N": "N",
|
||||||
|
"NNE": "NNE",
|
||||||
|
"NE": "NE",
|
||||||
|
"ENE": "ENE",
|
||||||
|
"E": "E",
|
||||||
|
"ESE": "ESE",
|
||||||
|
"SE": "SE",
|
||||||
|
"SSE": "SSE",
|
||||||
|
"S": "S",
|
||||||
|
"SSW": "SSW",
|
||||||
|
"SW": "SW",
|
||||||
|
"WSW": "WSW",
|
||||||
|
"W": "W",
|
||||||
|
"WNW": "WNW",
|
||||||
|
"NW": "NW",
|
||||||
|
"NNW": "NNW",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
||||||
|
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
|
||||||
|
}
|
129
tests/e2e/translations_spec.js
Normal file
129
tests/e2e/translations_spec.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const mlog = require("mocha-logger");
|
||||||
|
const translations = require("../../translations/translations.js");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const {JSDOM} = require("jsdom");
|
||||||
|
const express = require("express");
|
||||||
|
|
||||||
|
describe("Translations", function() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
const app = express();
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use("/translations", express.static(path.join(__dirname, "..", "..", "translations")));
|
||||||
|
|
||||||
|
server = app.listen(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have a translation file in the specified path", function() {
|
||||||
|
for(let language in translations) {
|
||||||
|
const file = fs.statSync(translations[language]);
|
||||||
|
expect(file.isFile()).to.be.equal(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mmm = {
|
||||||
|
name: "TranslationTest",
|
||||||
|
file(file) {
|
||||||
|
return `http://localhost:3000/${file}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Parsing language files through the Translator class", function() {
|
||||||
|
for(let language in translations) {
|
||||||
|
it(`should parse ${language}`, function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
|
||||||
|
Translator.load(mmm, translations[language], false, function() {
|
||||||
|
expect(Translator.translations[mmm.name]).to.be.an("object");
|
||||||
|
expect(Object.keys(Translator.translations[mmm.name]).length).to.be.at.least(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Same keys", function() {
|
||||||
|
let base;
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
|
||||||
|
Translator.load(mmm, translations.en, false, function() {
|
||||||
|
base = Object.keys(Translator.translations[mmm.name]).sort();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let language in translations) {
|
||||||
|
if (language === "en") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`Translation keys of ${language}`, function() {
|
||||||
|
let keys;
|
||||||
|
|
||||||
|
before(function(done){
|
||||||
|
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
|
||||||
|
Translator.load(mmm, translations[language], false, function() {
|
||||||
|
keys = Object.keys(Translator.translations[mmm.name]).sort();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`${language} keys should be in base`, function() {
|
||||||
|
keys.forEach(function(key) {
|
||||||
|
expect(base.indexOf(key)).to.be.at.least(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`${language} should contain all base keys`, function() {
|
||||||
|
// TODO: when all translations are fixed, use
|
||||||
|
// expect(keys).to.deep.equal(base);
|
||||||
|
// instead of the try-catch-block
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(keys).to.deep.equal(base);
|
||||||
|
} catch(e) {
|
||||||
|
if (e instanceof chai.AssertionError) {
|
||||||
|
const diff = base.filter(key => !keys.includes(key));
|
||||||
|
mlog.pending(`Missing Translations for language ${language}: ${diff}`);
|
||||||
|
this.skip();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
109
tests/unit/classes/class_spec.js
Normal file
109
tests/unit/classes/class_spec.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const path = require("path");
|
||||||
|
const {JSDOM} = require("jsdom");
|
||||||
|
|
||||||
|
describe("File js/class", function() {
|
||||||
|
describe("Test function cloneObject", function() {
|
||||||
|
let clone;
|
||||||
|
let dom;
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
dom = new JSDOM(`<script>var Log = {log: function() {}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {cloneObject} = dom.window;
|
||||||
|
clone = cloneObject;
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone object", function() {
|
||||||
|
const expected = {name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror"};
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.deep.equal(expected);
|
||||||
|
expect(expected === obj).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone array", function() {
|
||||||
|
const expected = [1, null, undefined, "TEST"];
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.deep.equal(expected);
|
||||||
|
expect(expected === obj).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone number", function() {
|
||||||
|
let expected = 1;
|
||||||
|
let obj = clone(expected);
|
||||||
|
expect(obj).to.equal(expected);
|
||||||
|
|
||||||
|
expected = 1.23;
|
||||||
|
obj = clone(expected);
|
||||||
|
expect(obj).to.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone string", function() {
|
||||||
|
const expected = "Perfect stranger";
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone undefined", function() {
|
||||||
|
const expected = undefined;
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone null", function() {
|
||||||
|
const expected = null;
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone nested object", function() {
|
||||||
|
const expected = {
|
||||||
|
name: "fewieden",
|
||||||
|
link: "https://github.com/fewieden",
|
||||||
|
versions: ["2.0", "2.1", "2.2"],
|
||||||
|
answerForAllQuestions: 42,
|
||||||
|
properties: {
|
||||||
|
items: [{foo: "bar"}, {lorem: "ipsum"}],
|
||||||
|
invalid: undefined,
|
||||||
|
nothing: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.deep.equal(expected);
|
||||||
|
expect(expected === obj).to.equal(false);
|
||||||
|
expect(expected.versions === obj.versions).to.equal(false);
|
||||||
|
expect(expected.properties === obj.properties).to.equal(false);
|
||||||
|
expect(expected.properties.items === obj.properties.items).to.equal(false);
|
||||||
|
expect(expected.properties.items[0] === obj.properties.items[0]).to.equal(false);
|
||||||
|
expect(expected.properties.items[1] === obj.properties.items[1]).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test lockstring code", function() {
|
||||||
|
let log;
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
log = dom.window.Log.log;
|
||||||
|
dom.window.Log.log = function cmp(str) {
|
||||||
|
expect(str).to.equal("lockStrings");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
dom.window.Log.log = log;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone object and log lockStrings", function() {
|
||||||
|
const expected = {name: "Module", lockStrings: "stringLock"};
|
||||||
|
const obj = clone(expected);
|
||||||
|
expect(obj).to.deep.equal(expected);
|
||||||
|
expect(expected === obj).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
17
tests/unit/classes/deprecated_spec.js
Normal file
17
tests/unit/classes/deprecated_spec.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const deprecated = require("../../../js/deprecated");
|
||||||
|
|
||||||
|
describe("Deprecated", function() {
|
||||||
|
it("should be an object", function() {
|
||||||
|
expect(deprecated).to.be.an("object");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain configs array with deprecated options as strings", function() {
|
||||||
|
expect(deprecated.configs).to.be.an("array");
|
||||||
|
for (let option of deprecated.configs) {
|
||||||
|
expect(option).to.be.an("string");
|
||||||
|
}
|
||||||
|
expect(deprecated.configs).to.include("kioskmode");
|
||||||
|
});
|
||||||
|
});
|
298
tests/unit/classes/translator_spec.js
Normal file
298
tests/unit/classes/translator_spec.js
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
const chai = require("chai");
|
||||||
|
const expect = chai.expect;
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const {JSDOM} = require("jsdom");
|
||||||
|
const express = require("express");
|
||||||
|
|
||||||
|
describe("Translator", function() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
const app = express();
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(function (req, res, next) {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "configs", "data")));
|
||||||
|
|
||||||
|
server = app.listen(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("translate", function() {
|
||||||
|
const translations = {
|
||||||
|
"MMM-Module": {
|
||||||
|
"Hello": "Hallo",
|
||||||
|
"Hello {username}": "Hallo {username}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const coreTranslations = {
|
||||||
|
"Hello": "XXX",
|
||||||
|
"Hello {username}": "XXX",
|
||||||
|
"FOO": "Foo",
|
||||||
|
"BAR {something}": "Bar {something}"
|
||||||
|
};
|
||||||
|
|
||||||
|
const translationsFallback = {
|
||||||
|
"MMM-Module": {
|
||||||
|
"Hello": "XXX",
|
||||||
|
"Hello {username}": "XXX",
|
||||||
|
"FOO": "XXX",
|
||||||
|
"BAR {something}": "XXX",
|
||||||
|
"A key": "A translation"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const coreTranslationsFallback = {
|
||||||
|
"FOO": "XXX",
|
||||||
|
"BAR {something}": "XXX",
|
||||||
|
"Hello": "XXX",
|
||||||
|
"Hello {username}": "XXX",
|
||||||
|
"A key": "XXX",
|
||||||
|
"Fallback": "core fallback"
|
||||||
|
};
|
||||||
|
|
||||||
|
function setTranslations(Translator) {
|
||||||
|
Translator.translations = translations;
|
||||||
|
Translator.coreTranslations = coreTranslations;
|
||||||
|
Translator.translationsFallback = translationsFallback;
|
||||||
|
Translator.coreTranslationsFallback = coreTranslationsFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should return custom module translation", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
let translation = Translator.translate({name: "MMM-Module"}, "Hello");
|
||||||
|
expect(translation).to.be.equal("Hallo");
|
||||||
|
translation = Translator.translate({name: "MMM-Module"}, "Hello {username}", {username: "fewieden"});
|
||||||
|
expect(translation).to.be.equal("Hallo fewieden");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return core translation", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
let translation = Translator.translate({name: "MMM-Module"}, "FOO");
|
||||||
|
expect(translation).to.be.equal("Foo");
|
||||||
|
translation = Translator.translate({name: "MMM-Module"}, "BAR {something}", {something: "Lorem Ipsum"});
|
||||||
|
expect(translation).to.be.equal("Bar Lorem Ipsum");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return custom module translation fallback", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
const translation = Translator.translate({name: "MMM-Module"}, "A key");
|
||||||
|
expect(translation).to.be.equal("A translation");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return core translation fallback", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
const translation = Translator.translate({name: "MMM-Module"}, "Fallback");
|
||||||
|
expect(translation).to.be.equal("core fallback");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return translation with placeholder for missing variables", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
const translation = Translator.translate({name: "MMM-Module"}, "Hello {username}");
|
||||||
|
expect(translation).to.be.equal("Hallo {username}");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return key if no translation was found", function(done) {
|
||||||
|
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
setTranslations(Translator);
|
||||||
|
const translation = Translator.translate({name: "MMM-Module"}, "MISSING");
|
||||||
|
expect(translation).to.be.equal("MISSING");
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("load", function() {
|
||||||
|
const mmm = {
|
||||||
|
name: "TranslationTest",
|
||||||
|
file(file) {
|
||||||
|
return `http://localhost:3000/translations/${file}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should load translations", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
const file = "TranslationTest.json";
|
||||||
|
|
||||||
|
Translator.load(mmm, file, false, function() {
|
||||||
|
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
||||||
|
expect(Translator.translations[mmm.name]).to.be.deep.equal(json);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load translation fallbacks", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
const file = "TranslationTest.json";
|
||||||
|
|
||||||
|
Translator.load(mmm, file, true, function() {
|
||||||
|
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
||||||
|
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal(json);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip comments", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
const file = "StripComments.json";
|
||||||
|
|
||||||
|
Translator.load(mmm, file, false, function() {
|
||||||
|
expect(Translator.translations[mmm.name]).to.be.deep.equal({
|
||||||
|
"FOO\"BAR": "Today",
|
||||||
|
"N": "N",
|
||||||
|
"E": "E",
|
||||||
|
"S": "S",
|
||||||
|
"W": "W"
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not load translations, if module fallback exists", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator, XMLHttpRequest} = dom.window;
|
||||||
|
const file = "TranslationTest.json";
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.send = function() {
|
||||||
|
throw "Shouldn't load files";
|
||||||
|
};
|
||||||
|
|
||||||
|
Translator.translationsFallback[mmm.name] = {
|
||||||
|
Hello: "Hallo"
|
||||||
|
};
|
||||||
|
|
||||||
|
Translator.load(mmm, file, false, function() {
|
||||||
|
expect(Translator.translations[mmm.name]).to.be.undefined;
|
||||||
|
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal({
|
||||||
|
Hello: "Hallo"
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadCoreTranslations", function() {
|
||||||
|
it("should load core translations and fallback", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
Translator.loadCoreTranslations("en");
|
||||||
|
|
||||||
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||||
|
setTimeout(function() {
|
||||||
|
expect(Translator.coreTranslations).to.be.deep.equal(en);
|
||||||
|
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load core fallback if language cannot be found", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
Translator.loadCoreTranslations("MISSINGLANG");
|
||||||
|
|
||||||
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||||
|
setTimeout(function() {
|
||||||
|
expect(Translator.coreTranslations).to.be.deep.equal({});
|
||||||
|
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadCoreTranslationsFallback", function() {
|
||||||
|
it("should load core translations fallback", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
Translator.loadCoreTranslationsFallback();
|
||||||
|
|
||||||
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||||
|
setTimeout(function() {
|
||||||
|
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load core fallback if language cannot be found", function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {Translator} = dom.window;
|
||||||
|
Translator.loadCoreTranslations();
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
expect(Translator.coreTranslationsFallback).to.be.deep.equal({});
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
41
tests/unit/classes/utils_spec.js
Normal file
41
tests/unit/classes/utils_spec.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
var chai = require("chai");
|
||||||
|
var expect = chai.expect;
|
||||||
|
var Utils = require("../../../js/utils.js");
|
||||||
|
var colors = require("colors/safe");
|
||||||
|
|
||||||
|
describe("Utils", function() {
|
||||||
|
describe("colors", function() {
|
||||||
|
var colorsEnabled = colors.enabled;
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
colors.enabled = colorsEnabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have info, warn and error properties", function() {
|
||||||
|
expect(Utils.colors).to.have.property("info");
|
||||||
|
expect(Utils.colors).to.have.property("warn");
|
||||||
|
expect(Utils.colors).to.have.property("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("properties should be functions", function() {
|
||||||
|
expect(Utils.colors.info).to.be.a("function");
|
||||||
|
expect(Utils.colors.warn).to.be.a("function");
|
||||||
|
expect(Utils.colors.error).to.be.a("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should print colored message in supported consoles", function() {
|
||||||
|
colors.enabled = true;
|
||||||
|
expect(Utils.colors.info("some informations")).to.be.equal("\u001b[34msome informations\u001b[39m");
|
||||||
|
expect(Utils.colors.warn("a warning")).to.be.equal("\u001b[33ma warning\u001b[39m");
|
||||||
|
expect(Utils.colors.error("ERROR!")).to.be.equal("\u001b[31mERROR!\u001b[39m");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should print message in unsupported consoles", function() {
|
||||||
|
colors.enabled = false;
|
||||||
|
expect(Utils.colors.info("some informations")).to.be.equal("some informations");
|
||||||
|
expect(Utils.colors.warn("a warning")).to.be.equal("a warning");
|
||||||
|
expect(Utils.colors.error("ERROR!")).to.be.equal("ERROR!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,51 +0,0 @@
|
|||||||
var chai = require("chai");
|
|
||||||
var expect = chai.expect;
|
|
||||||
var jsClass = require("../../../js/class.js");
|
|
||||||
|
|
||||||
describe("File js/class", function() {
|
|
||||||
describe("Test function cloneObject", function() {
|
|
||||||
var cloneObject = jsClass._test.cloneObject;
|
|
||||||
|
|
||||||
it("should be return equals object", function() {
|
|
||||||
var expected = {name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror"};
|
|
||||||
var obj = {};
|
|
||||||
obj = cloneObject(expected);
|
|
||||||
expect(expected).to.deep.equal(obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be return equals int", function() {
|
|
||||||
var expected = 1;
|
|
||||||
var obj = {};
|
|
||||||
obj = cloneObject(expected);
|
|
||||||
expect(expected).to.equal(obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be return equals string", function() {
|
|
||||||
var expected = "Perfect stranger";
|
|
||||||
var obj = {};
|
|
||||||
obj = cloneObject(expected);
|
|
||||||
expect(expected).to.equal(obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be return equals undefined", function() {
|
|
||||||
var expected = undefined;
|
|
||||||
var obj = {};
|
|
||||||
obj = cloneObject(expected);
|
|
||||||
expect(undefined).to.equal(obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
// CoverageME
|
|
||||||
/*
|
|
||||||
context("Test lockstring code", function() {
|
|
||||||
it("should be return equals object", function() {
|
|
||||||
var expected = {name: "Module", lockStrings: "stringLock"};
|
|
||||||
var obj = {};
|
|
||||||
obj = cloneObject(expected);
|
|
||||||
expect(expected).to.deep.equal(obj);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
|||||||
var chai = require("chai");
|
const chai = require("chai");
|
||||||
var expect = chai.expect;
|
const expect = chai.expect;
|
||||||
var classMM = require("../../../js/class.js"); // require for load module.js
|
const path = require("path");
|
||||||
var moduleMM = require("../../../js/module.js")
|
const {JSDOM} = require("jsdom");
|
||||||
|
|
||||||
describe("Test function cmpVersions in js/module.js", function() {
|
describe("Test function cmpVersions in js/module.js", function() {
|
||||||
|
let cmp;
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
const dom = new JSDOM(`<script>var Class = {extend: function() { return {}; }};</script>\
|
||||||
|
<script src="${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`, { runScripts: "dangerously",
|
||||||
|
resources: "usable" });
|
||||||
|
dom.window.onload = function() {
|
||||||
|
const {cmpVersions} = dom.window;
|
||||||
|
cmp = cmpVersions;
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
it("should return -1 when comparing 2.1 to 2.2", function() {
|
it("should return -1 when comparing 2.1 to 2.2", function() {
|
||||||
expect(moduleMM._test.cmpVersions("2.1", "2.2")).to.equal(-1);
|
expect(cmp("2.1", "2.2")).to.equal(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be return 0 when comparing 2.2 to 2.2", function() {
|
it("should be return 0 when comparing 2.2 to 2.2", function() {
|
||||||
expect(moduleMM._test.cmpVersions("2.2", "2.2")).to.equal(0);
|
expect(cmp("2.2", "2.2")).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be return 1 when comparing 1.1 to 1.0", function() {
|
it("should be return 1 when comparing 1.1 to 1.0", function() {
|
||||||
expect(moduleMM._test.cmpVersions("1.1", "1.0")).to.equal(1);
|
expect(cmp("1.1", "1.0")).to.equal(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
var fs = require("fs");
|
|
||||||
var path = require("path");
|
|
||||||
var chai = require("chai");
|
|
||||||
var expect = chai.expect;
|
|
||||||
var mlog = require("mocha-logger");
|
|
||||||
|
|
||||||
describe("Translations have the same keys as en.js", function() {
|
|
||||||
var translations = require("../../../translations/translations.js");
|
|
||||||
var base = JSON.parse(stripComments(fs.readFileSync("translations/en.json", "utf8")));
|
|
||||||
var baseKeys = Object.keys(base).sort();
|
|
||||||
|
|
||||||
Object.keys(translations).forEach(function(tr) {
|
|
||||||
var fileName = translations[tr];
|
|
||||||
var fileContent = stripComments(fs.readFileSync(fileName, "utf8"));
|
|
||||||
var fileTranslations = JSON.parse(fileContent);
|
|
||||||
var fileKeys = Object.keys(fileTranslations).sort();
|
|
||||||
|
|
||||||
it(fileName + " keys should be in base", function() {
|
|
||||||
fileKeys.forEach(function(key) {
|
|
||||||
expect( baseKeys.indexOf(key) ).to.be.at.least(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(fileName + " should contain all base keys", function() {
|
|
||||||
var test = this;
|
|
||||||
baseKeys.forEach(function(key) {
|
|
||||||
// TODO: when all translations are fixed, use
|
|
||||||
// expect(fileKeys).to.deep.equal(baseKeys);
|
|
||||||
// instead of the try-catch-block
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(fileKeys).to.deep.equal(baseKeys);
|
|
||||||
} catch(e) {
|
|
||||||
if (e instanceof chai.AssertionError) {
|
|
||||||
diff = baseKeys.filter(function(x) { return fileKeys.indexOf(x) < 0 });
|
|
||||||
mlog.pending("Missing Translations for language " + tr + ": ", diff);
|
|
||||||
test.skip();
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copied from js/translator.js
|
|
||||||
function stripComments(str, opts) {
|
|
||||||
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
|
|
||||||
|
|
||||||
var singleComment = 1;
|
|
||||||
var multiComment = 2;
|
|
||||||
|
|
||||||
function stripWithoutWhitespace() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripWithWhitespace(str, start, end) {
|
|
||||||
return str.slice(start, end).replace(/\S/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = opts || {};
|
|
||||||
|
|
||||||
var currentChar;
|
|
||||||
var nextChar;
|
|
||||||
var insideString = false;
|
|
||||||
var insideComment = false;
|
|
||||||
var offset = 0;
|
|
||||||
var ret = "";
|
|
||||||
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
|
|
||||||
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
currentChar = str[i];
|
|
||||||
nextChar = str[i + 1];
|
|
||||||
|
|
||||||
if (!insideComment && currentChar === "\"") {
|
|
||||||
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
|
|
||||||
if (!escaped) {
|
|
||||||
insideString = !insideString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insideString) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!insideComment && currentChar + nextChar === "//") {
|
|
||||||
ret += str.slice(offset, i);
|
|
||||||
offset = i;
|
|
||||||
insideComment = singleComment;
|
|
||||||
i++;
|
|
||||||
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
|
|
||||||
i++;
|
|
||||||
insideComment = false;
|
|
||||||
ret += strip(str, offset, i);
|
|
||||||
offset = i;
|
|
||||||
continue;
|
|
||||||
} else if (insideComment === singleComment && currentChar === "\n") {
|
|
||||||
insideComment = false;
|
|
||||||
ret += strip(str, offset, i);
|
|
||||||
offset = i;
|
|
||||||
} else if (!insideComment && currentChar + nextChar === "/*") {
|
|
||||||
ret += str.slice(offset, i);
|
|
||||||
offset = i;
|
|
||||||
insideComment = multiComment;
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
|
|
||||||
i++;
|
|
||||||
insideComment = false;
|
|
||||||
ret += strip(str, offset, i + 1);
|
|
||||||
offset = i + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
|
|
||||||
}
|
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² update beskikbaar.",
|
"UPDATE_NOTIFICATION": "MagicMirror² update beskikbaar.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Update beskikbaar vir MODULE_NAME module.",
|
"UPDATE_NOTIFICATION_MODULE": "Update beskikbaar vir {MODULE_NAME} module.",
|
||||||
"UPDATE_INFO": "Die huidige installasie is COMMIT_COUNT agter op die BRANCH_NAME branch."
|
"UPDATE_INFO": "Die huidige installasie is {COMMIT_COUNT} agter op die {BRANCH_NAME} branch."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "ССЗ",
|
"NNW": "ССЗ",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Налична актуализация за MagicMirror².",
|
"UPDATE_NOTIFICATION": "Налична актуализация за MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за MODULE_NAME модул.",
|
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за {MODULE_NAME} модул.",
|
||||||
"UPDATE_INFO": "Текущата инсталация е изостанала с COMMIT_COUNT къмита на клон BRANCH_NAME."
|
"UPDATE_INFO": "Текущата инсталация е изостанала с {COMMIT_COUNT} къмита на клон {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNO",
|
"NNW": "NNO",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² actualizació disponible.",
|
"UPDATE_NOTIFICATION": "MagicMirror² actualizació disponible.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualizació per al mòdul MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualizació per al mòdul {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "La teva instal·lació actual està COMMIT_COUNT canvis darrere de la branca BRANCH_NAME."
|
"UPDATE_INFO": "La teva instal·lació actual està {COMMIT_COUNT} canvis darrere de la branca {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "SSZ",
|
"NNW": "SSZ",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Dostupná aktualizace pro MagicMirror².",
|
"UPDATE_NOTIFICATION": "Dostupná aktualizace pro MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Dostupná aktualizace pro modul MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Dostupná aktualizace pro modul {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Současná instalace je na větvi BRANCH_NAME pozadu o COMMIT_COUNT."
|
"UPDATE_INFO": "Současná instalace je na větvi {BRANCH_NAME} pozadu o {COMMIT_COUNT}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "GoGoGe",
|
"NNW": "GoGoGe",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² mwy diweddar yn barod.",
|
"UPDATE_NOTIFICATION": "MagicMirror² mwy diweddar yn barod.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Mae diweddaraiad ar gyfer y modiwl MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Mae diweddaraiad ar gyfer y modiwl {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Mae'r fersiwn bresenol COMMIT_COUNT commit tu ôl i'r gangen BRANCH_NAME."
|
"UPDATE_INFO": "Mae'r fersiwn bresenol {COMMIT_COUNT} commit tu ôl i'r gangen {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,6 @@
|
|||||||
|
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² opdatering tilgængelig.",
|
"UPDATE_NOTIFICATION": "MagicMirror² opdatering tilgængelig.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for MODULE_NAME modulet.",
|
"UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for {MODULE_NAME} modulet.",
|
||||||
"UPDATE_INFO": "Den nuværende installation er COMMIT_COUNT bagud på BRANCH_NAME branch'en."
|
"UPDATE_INFO": "Den nuværende installation er {COMMIT_COUNT} bagud på {BRANCH_NAME} branch'en."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
|
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das MODULE_NAME Modul verfügbar.",
|
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.",
|
||||||
"UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch."
|
"UPDATE_INFO": "Die aktuelle Installation ist {COMMIT_COUNT} hinter dem {BRANCH_NAME} branch."
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
"TODAY": "Today",
|
"TODAY": "Today",
|
||||||
"TOMORROW": "Tomorrow",
|
"TOMORROW": "Tomorrow",
|
||||||
"DAYAFTERTOMORROW": "The day after tomorrow",
|
"DAYAFTERTOMORROW": "In 2 days",
|
||||||
"RUNNING": "Ends in",
|
"RUNNING": "Ends in",
|
||||||
"EMPTY": "No upcoming events.",
|
"EMPTY": "No upcoming events.",
|
||||||
|
|
||||||
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
|
||||||
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
|
"UPDATE_INFO": "The current installation is {COMMIT_COUNT} behind on the {BRANCH_NAME} branch."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNO",
|
"NNW": "NNO",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
|
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Tu actual instalación está COMMIT_COUNT cambios detrás de la rama BRANCH_NAME."
|
"UPDATE_INFO": "Tu actual instalación está {COMMIT_COUNT} cambios detrás de la rama {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "Põhjaloe",
|
"NNW": "Põhjaloe",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror²´le uuendus saadaval.",
|
"UPDATE_NOTIFICATION": "MagicMirror²´le uuendus saadaval.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Uuendus saadaval MODULE_NAME moodulile.",
|
"UPDATE_NOTIFICATION_MODULE": "Uuendus saadaval {MODULE_NAME} moodulile.",
|
||||||
"UPDATE_INFO": "Praegune paigaldus on COMMIT_COUNT tagapool BRANCH_NAME harul."
|
"UPDATE_INFO": "Praegune paigaldus on {COMMIT_COUNT} tagapool {BRANCH_NAME} harul."
|
||||||
}
|
}
|
||||||
|
@ -25,5 +25,5 @@
|
|||||||
"NNW": "PPL",
|
"NNW": "PPL",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² päivitys saatavilla.",
|
"UPDATE_NOTIFICATION": "MagicMirror² päivitys saatavilla.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille MODULE_NAME."
|
"UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille {MODULE_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNO",
|
"NNW": "NNO",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
|
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module MODULE_NAME .",
|
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME} .",
|
||||||
"UPDATE_INFO": "L'installation actuelle est COMMIT_COUNT en retard sur la branche BRANCH_NAME ."
|
"UPDATE_INFO": "L'installation actuelle est {COMMIT_COUNT} en retard sur la branche {BRANCH_NAME} ."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "ÉÉNy",
|
"NNW": "ÉÉNy",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² elérhető egy frissítés!",
|
"UPDATE_NOTIFICATION": "MagicMirror² elérhető egy frissítés!",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "A frissítés MODULE_NAME modul néven érhető el.",
|
"UPDATE_NOTIFICATION_MODULE": "A frissítés {MODULE_NAME} modul néven érhető el.",
|
||||||
"UPDATE_INFO": "A jelenlegi telepítés COMMIT_COUNT mögött BRANCH_NAME ágon található."
|
"UPDATE_INFO": "A jelenlegi telepítés {COMMIT_COUNT} mögött {BRANCH_NAME} ágon található."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "UBL",
|
"NNW": "UBL",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Memperbarui MagicMirror² tersedia.",
|
"UPDATE_NOTIFICATION": "Memperbarui MagicMirror² tersedia.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Memperbarui tersedia untuk modul MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Memperbarui tersedia untuk modul {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Instalasi saat ini tertinggal COMMIT_COUNT pada cabang BRANCH_NAME."
|
"UPDATE_INFO": "Instalasi saat ini tertinggal {COMMIT_COUNT} pada cabang {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "NNV",
|
"NNW": "NNV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² uppfærsla í boði.",
|
"UPDATE_NOTIFICATION": "MagicMirror² uppfærsla í boði.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Uppfærsla í boði fyrir MODULE_NAME module.",
|
"UPDATE_NOTIFICATION_MODULE": "Uppfærsla í boði fyrir {MODULE_NAME} module.",
|
||||||
"UPDATE_INFO": "Núverandi kerfi er COMMIT_COUNT á eftir BRANCH_NAME branchinu."
|
"UPDATE_INFO": "Núverandi kerfi er {COMMIT_COUNT} á eftir {BRANCH_NAME} branchinu."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "북북서풍",
|
"NNW": "북북서풍",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "새로운 MagicMirror² 업데이트가 있습니다.",
|
"UPDATE_NOTIFICATION": "새로운 MagicMirror² 업데이트가 있습니다.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "MODULE_NAME 모듈에서 사용 가능한 업데이트 입니다.",
|
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} 모듈에서 사용 가능한 업데이트 입니다.",
|
||||||
"UPDATE_INFO": "설치할 COMMIT_COUNT 는 BRANCH_NAME 분기에 해당됩니다."
|
"UPDATE_INFO": "설치할 {COMMIT_COUNT} 는 {BRANCH_NAME} 분기에 해당됩니다."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNV",
|
"NNW": "NNV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror²-oppdatering er tilgjengelig.",
|
"UPDATE_NOTIFICATION": "MagicMirror²-oppdatering er tilgjengelig.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Nåværende installasjon er COMMIT_COUNT bak BRANCH_NAME grenen."
|
"UPDATE_INFO": "Nåværende installasjon er {COMMIT_COUNT} bak {BRANCH_NAME} grenen."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
|
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor MODULE_NAME module.",
|
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor {MODULE_NAME} module.",
|
||||||
"UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch."
|
"UPDATE_INFO": "De huidige installatie loopt {COMMIT_COUNT} achter op de {BRANCH_NAME} branch."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "NNV",
|
"NNW": "NNV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² oppdatering er tilgjengeleg.",
|
"UPDATE_NOTIFICATION": "MagicMirror² oppdatering er tilgjengeleg.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengeleg for modulen MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengeleg for modulen {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "noverande installasjon er COMMIT_COUNT bak BRANCH_NAME greinen."
|
"UPDATE_INFO": "noverande installasjon er {COMMIT_COUNT} bak {BRANCH_NAME} greinen."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Dostępna jest aktualizacja MagicMirror².",
|
"UPDATE_NOTIFICATION": "Dostępna jest aktualizacja MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Dostępna jest aktualizacja modułu MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Dostępna jest aktualizacja modułu {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Zainstalowana wersja odbiega o COMMIT_COUNT commitów od gałęzi BRANCH_NAME."
|
"UPDATE_INFO": "Zainstalowana wersja odbiega o {COMMIT_COUNT} commitów od gałęzi {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,6 @@
|
|||||||
"NNW": "NNO",
|
"NNW": "NNO",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Atualização do MagicMirror² disponível.",
|
"UPDATE_NOTIFICATION": "Atualização do MagicMirror² disponível.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Atualização para o módulo MODULE_NAME disponível.",
|
"UPDATE_NOTIFICATION_MODULE": "Atualização para o módulo {MODULE_NAME} disponível.",
|
||||||
"UPDATE_INFO": "A instalação atual está COMMIT_COUNT atrasada no branch BRANCH_NAME."
|
"UPDATE_INFO": "A instalação atual está {COMMIT_COUNT} atrasada no branch {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNW",
|
"NNW": "NNW",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Un update este disponibil pentru MagicMirror².",
|
"UPDATE_NOTIFICATION": "Un update este disponibil pentru MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Un update este disponibil pentru modulul MODULE_NAME.",
|
"UPDATE_NOTIFICATION_MODULE": "Un update este disponibil pentru modulul {MODULE_NAME}.",
|
||||||
"UPDATE_INFO": "Există COMMIT_COUNT commit-uri noi pe branch-ul BRANCH_NAME."
|
"UPDATE_INFO": "Există {COMMIT_COUNT} commit-uri noi pe branch-ul {BRANCH_NAME}."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "ССЗ",
|
"NNW": "ССЗ",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².",
|
"UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для MODULE_NAME модуля.",
|
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для {MODULE_NAME} модуля.",
|
||||||
"UPDATE_INFO": "Данная инсталляция позади BRANCH_NAME ветки на COMMIT_COUNT коммитов."
|
"UPDATE_INFO": "Данная инсталляция позади {BRANCH_NAME} ветки на {COMMIT_COUNT} коммитов."
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"NNW": "NNV",
|
"NNW": "NNV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² uppdatering finns tillgänglig.",
|
"UPDATE_NOTIFICATION": "MagicMirror² uppdatering finns tillgänglig.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Uppdatering finns tillgänglig av MODULE_NAME modulen.",
|
"UPDATE_NOTIFICATION_MODULE": "Uppdatering finns tillgänglig av {MODULE_NAME} modulen.",
|
||||||
"UPDATE_INFO": "Denna installation ligger COMMIT_COUNT steg bakom BRANCH_NAME grenen."
|
"UPDATE_INFO": "Denna installation ligger {COMMIT_COUNT} steg bakom {BRANCH_NAME} grenen."
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@
|
|||||||
"NNW": "北偏西风",
|
"NNW": "北偏西风",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² 有新的更新",
|
"UPDATE_NOTIFICATION": "MagicMirror² 有新的更新",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "模块 MODULE_NAME 可更新",
|
"UPDATE_NOTIFICATION_MODULE": "模块 {MODULE_NAME} 可更新",
|
||||||
"UPDATE_INFO": "当前已安装版本为 COMMIT_COUNT 落后于分支 BRANCH_NAME "
|
"UPDATE_INFO": "当前已安装版本为 {COMMIT_COUNT} 落后于分支 {BRANCH_NAME} "
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user