Compare commits

...

150 Commits

Author SHA1 Message Date
Michael Teeuw
60b9a5b9da Merge pull request #1245 from MichMich/electron-downgrade
v2.3.1
2018-04-01 19:12:25 +02:00
Michael Teeuw
eaaa62a7f3 Downgrade Electron. 2018-04-01 19:05:38 +02:00
Michael Teeuw
fb0cc61e09 Merge pull request #1241 from MichMich/develop
Release 2.3.0
2018-04-01 14:16:36 +02:00
Michael Teeuw
be29b5daf8 Prepare for release 2.3.0 2018-04-01 14:08:38 +02:00
Michael Teeuw
f010adabd0 Merge pull request #1232 from Kiina/develop
Update electron to 1.7.13
2018-03-28 12:27:05 +02:00
Dominic Dey-Marckmann
c93b263b1f Update electron to 1.7.13 2018-03-27 21:09:08 +02:00
Michael Teeuw
15f34d6b54 Update CHANGELOG.md 2018-03-25 14:56:20 +02:00
Michael Teeuw
d0eeb55999 Merge pull request #1186 from BonySimon/patch-1
Fix exception on translation of objects
2018-03-25 14:55:52 +02:00
Michael Teeuw
08c0d39b23 Update translator.js 2018-03-25 14:55:42 +02:00
Michael Teeuw
d0ecde3277 Merge pull request #1176 from relm923/forecast_max_days
Forecast - Max Days
2018-03-25 14:53:30 +02:00
Michael Teeuw
45ec57afd7 Merge branch 'develop' into forecast_max_days 2018-03-25 14:52:42 +02:00
Michael Teeuw
b0cd053083 Merge pull request #1202 from iampranavsethi/currentweather-module-updates
Currentweather module updates
2018-03-25 14:52:05 +02:00
Michael Teeuw
698a11be58 Merge branch 'develop' into currentweather-module-updates 2018-03-25 14:51:47 +02:00
Michael Teeuw
efb08fb1e1 Update CHANGELOG.md 2018-03-25 14:51:18 +02:00
Michael Teeuw
f89bc8422e Merge pull request #1224 from bacongobbler/clearer-install-question
capitalize "y/n" for clearer intent
2018-03-25 14:47:35 +02:00
Michael Teeuw
4bf4889a08 Merge pull request #1225 from moham96/patch-1
use shallow clone
2018-03-25 14:47:09 +02:00
Michael Teeuw
01ab8ba38e Merge pull request #1227 from E3V3A/patch-4
removed known issues as they are closed
2018-03-25 14:46:44 +02:00
E:V:A
ae6d15e812 removed known issues as they are closed 2018-03-25 12:07:29 +03:00
MOHAMMAD RASIM
1abfbe1d34 use shallow clone
not need to download the whole repo history
2018-03-24 16:11:57 +03:00
Matthew Fisher
d3095297c2 capitalize "y/n" for clearer intent
If you press enter, `choice` is an empty string and will default to "no". The convention is to capitalize the default answer so users know what happens when they auto-accept prompts.
2018-03-23 16:29:37 -07:00
Michael Teeuw
79d40d5644 Merge pull request #1199 from ThomasMirlacher/develop
Add dc:date to parsing in newsfeed module, which allows parsing of mo…
2018-03-21 12:17:23 +01:00
Thomas Mirlacher
008e305a84 use doublequotes. 2018-03-21 11:58:09 +01:00
Michael Teeuw
0379611edd Merge pull request #1218 from kjb085/kb/calendar-adv-filter
Add advanced filtering to excludedEvents
2018-03-21 11:34:41 +01:00
Kenn Breece
96d883c1c7 Merge branch 'develop' into kb/calendar-adv-filter 2018-03-20 15:38:16 -04:00
Kenn Breece
be0f262e37 Add advanced filtering to excludedEvents 2018-03-18 23:54:23 -04:00
Michael Teeuw
1676adf071 Merge pull request #1209 from E3V3A/patch-3
update node stable to 9.x
2018-03-14 10:26:25 +01:00
E:V:A
a5d5630067 update node stable to 9.x 2018-03-14 11:24:41 +02:00
Pranav Sethi
275956caba Fixed typos in README.md for currentweather. 2018-03-12 22:22:45 -04:00
Michael Teeuw
bf8ed87fc9 Merge pull request #1197 from ptz0n/patch-1
Update README.md
2018-03-12 18:34:21 +01:00
Michael Teeuw
fb3afac097 Merge pull request #1200 from djgalloway/wip-apt-y
Assume yes when installing deps on Raspberry Pi via apt-get
2018-03-12 18:33:36 +01:00
Michael Teeuw
eda8b037a9 Merge pull request #1205 from Tajnymag/develop
Added yarn support
2018-03-12 18:32:31 +01:00
Marek Lukáš
8ef14f7a54 Added changes from [9974e35] to CHANGELOG 2018-03-12 15:33:30 +01:00
Marek Lukáš
9974e35656 Added yarn support 2018-03-12 15:28:43 +01:00
Pranav Sethi
15bc5431b6 Fixed Trailling Spaces 2018-03-12 05:25:33 -04:00
Pranav Sethi
f767531d89 Fixed Trailling Spaces 2018-03-12 05:25:01 -04:00
Pranav Sethi
7285ada9dd Added feels like and kmph wind for currentweather module 2018-03-12 05:06:16 -04:00
Pranav Sethi
b9b9773df9 Merge branch 'develop' into currentweather-module-updates 2018-03-12 03:56:44 -04:00
David Galloway
c29a83b259 Assume yes when installing deps on Raspberry Pi via apt-get
Signed-off-by: David Galloway <dgallowa@redhat.com>
2018-03-11 13:17:33 -04:00
Thomas Mirlacher
fa45e66da6 Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds. 2018-03-10 00:30:45 +01:00
Erik Eng
7cbcdddac9 Merge branch 'develop' into patch-1 2018-03-07 09:18:07 +01:00
Erik Eng
a2f17900da Update README.md
Correct manual installation step.
2018-03-07 09:13:33 +01:00
Michael Teeuw
fb7e97b8ad Merge pull request #1190 from E3V3A/patch-1
made the module "this" instances into a table
2018-03-06 09:36:04 +01:00
Michael Teeuw
0c92a8a8e9 Merge pull request #1191 from E3V3A/patch-2
fix md header
2018-03-06 09:35:34 +01:00
Michael Teeuw
dcc59380e5 Merge pull request #1194 from bastilimbach/develop
Remove old docker config and link to docker repository
2018-03-06 09:33:50 +01:00
Sebastian Limbach
f9bf25f96d Add changes 2018-03-04 14:07:44 +01:00
Sebastian Limbach
cbcbea8b08 Remove old docker config 2018-03-04 13:59:45 +01:00
Pranav Sethi
62eb4f20da Fixed Trailling Spaces to Pass checks 2018-03-03 05:08:12 -05:00
Pranav Sethi
43d5311e5e Fixed Trailling Spaces 2018-03-03 05:00:26 -05:00
Pranav Sethi
a6f08a09d5 Added Feels Like and Windspeed in KMPH 2018-03-03 04:52:22 -05:00
Pranav Sethi
d93f5d7785 Added Feels Like and Windspeed in KMPH 2018-03-03 04:21:24 -05:00
E:V:A
c7170e6dc2 fix md header 2018-03-01 11:53:10 +02:00
E:V:A
bcf3ca7339 made the module "this" instances into a table 2018-03-01 09:44:57 +02:00
Steelskin3
0388f5787a Fix exception on translation of objects
Sometimes, the content of translations[module.name][key] is not a string but an entire object. It cause a crash on template.replace(...) of createStringFromTemplate(...). It is currently the case of MMM-Voice-Control and the previous commit have broked this module (10 month ago... I am surprised to be the first founded it)
2018-02-23 11:51:59 +01:00
Michael Teeuw
6514df9d96 Merge pull request #1184 from patoberli/patch-1
Update README.md
2018-02-20 11:29:27 +01:00
Michael Teeuw
8f5b9869dc Merge branch 'develop' into patch-1 2018-02-20 11:28:58 +01:00
patoberli
580c5fe23f Update README.md
Added a hint to use the full and not lite version of Raspbian, as the GUI is missing and thus ndm (electron) can't start after the installation.
2018-02-17 21:54:31 +01:00
Michael Teeuw
b0af5b26ba Merge pull request #1183 from fewieden/feature/translations-update
translations update
2018-02-17 10:30:30 +01:00
Michael Teeuw
9e898932f6 Merge pull request #1182 from fewieden/feature/automated-tests
Automated tests
2018-02-17 10:29:23 +01:00
fewieden
1f873b93f6 changelog, linting 2018-02-17 10:17:59 +01:00
fewieden
f414707f11 update translations for updatenotifications 2018-02-17 10:14:37 +01:00
fewieden
505825056c use translation template for updatenotifications 2018-02-17 10:10:57 +01:00
fewieden
38f7716738 linting 2018-02-17 09:20:34 +01:00
fewieden
a69d08b554 changelog 2018-02-17 09:18:12 +01:00
fewieden
3ccdb64833 deprecated unit tests 2018-02-16 22:09:15 +01:00
fewieden
78d8bff599 clone array, clone nested object, add safe checks for objects (memory address) 2018-02-16 19:58:28 +01:00
fewieden
a756fed70b fixed and reenabled lockstring test 2018-02-16 08:43:27 +01:00
fewieden
3e2a1e3548 linting 2018-02-16 00:08:11 +01:00
fewieden
96b2f2b3a4 clone object unit test 2018-02-16 00:01:02 +01:00
fewieden
d81d7d4f68 compare version unit test 2018-02-15 23:53:57 +01:00
fewieden
20244c4fb5 translations integration test 2018-02-15 23:22:52 +01:00
Michael Teeuw
b50d31ffe2 Merge pull request #1178 from E3V3A/develop
Added ToC
2018-02-13 09:24:36 +01:00
fewieden
d709a44960 strip comments unit test 2018-02-13 07:17:46 +01:00
E:V:A
4e4d07ced6 Added ToC
ToDo: Fix ToC links to headers
2018-02-11 18:24:31 +02:00
fewieden
d775bc9d7e loadCoreTranslationsFallback unit test 2018-02-11 09:08:09 +01:00
fewieden
85528761eb only load core callback if there is one available 2018-02-11 09:07:41 +01:00
fewieden
ad3eac9ddb loadcoretransations unit test 2018-02-11 08:58:02 +01:00
Reagan Elm
613f9fccd2 Update changelog 2018-02-10 20:56:10 -05:00
Reagan Elm
3d1741c904 Respect maxNumberOfDays regardless of endpoint 2018-02-10 20:53:38 -05:00
fewieden
26be14ba67 load unit test 2018-02-10 20:33:22 +01:00
fewieden
daa0755920 add missing parameter in documentation 2018-02-10 12:38:55 +01:00
fewieden
305d60e09b translator unit tests 2018-02-10 12:32:43 +01:00
fewieden
d0029efd02 utils unit tests 2018-02-10 12:30:33 +01:00
fewieden
fb4d42bf5b moved test 2018-02-10 12:28:30 +01:00
Michael Teeuw
20eec53b14 Add Manifesto. 2018-02-07 13:04:10 +01:00
Michael Teeuw
8343db44db Merge pull request #1173 from vvzvlad/vvzvlad_local
add variable morning afternoon times
2018-02-07 12:32:42 +01:00
vvzvlad
649652e373 Merge branch 'develop' into vvzvlad_local 2018-02-07 14:10:43 +03:00
vvzvlad
e37ed7c32d add variable morning afternoon times 2018-02-07 14:06:26 +03:00
vvzvlad
f9a525068b add variable morning afternoon times 2018-02-05 18:15:02 +03:00
Michael Teeuw
aa11e6d62e Merge pull request #1166 from henrysun18/develop
Show remote compliments on boot
2018-02-01 08:20:22 +01:00
henrysun18
6802d152da Show remote compliments at boot instead of after one updateInterval 2018-01-31 23:19:47 -05:00
Michael Teeuw
3b40f393d8 Merge pull request #1163 from pinsdorf/patch-1
corrected link to 3rd party modules wiki page
2018-01-30 13:49:37 +01:00
pinsdorf
020443ae8a corrected link to 3rd party modules wiki page
The link to the 3rd Party Modules still directs to the old wiki page, where use has to follow another link to the right page. Updated the link in README.md to navigate to the right wiki page instantly.
2018-01-30 10:59:18 +01:00
Michael Teeuw
1d0baccffc Merge pull request #1161 from thobach/master
Allow to scroll in full page article view of default newsfeed module
2018-01-30 09:47:33 +01:00
Thomas Bachmann
5426f0f329 Updated documentation for scroll mode in newsfeed module 2018-01-29 21:41:43 +01:00
Thomas Bachmann
790249dd1a Merge branch 'MichMich/develop' 2018-01-29 21:37:25 +01:00
Thomas Bachmann
446a201d25 Allow to scroll articles with gesture events 2018-01-29 21:26:34 +01:00
Thomas Bachmann
b6538d5e18 Merge remote-tracking branch 'MichMich/master' 2018-01-29 18:54:32 +01:00
Michael Teeuw
edd6043059 Fail PRs that are sent to the master branch. 2018-01-26 12:12:44 +01:00
Michael Teeuw
fe4ffeb7f1 Text cleanup. 2018-01-25 20:45:25 +01:00
Michael Teeuw
27b3875bfb Changed missing Changlog text. 2018-01-25 20:38:42 +01:00
Michael Teeuw
e2dbe8a0a2 Minor fixes. 2018-01-25 20:07:51 +01:00
Michael Teeuw
29fc7910b7 Remove Jest. Update dangerfile.js. 2018-01-25 19:51:23 +01:00
Michael Teeuw
22e2fdc707 Jest implementation to get danger.js to work. 2018-01-25 19:43:09 +01:00
Michael Teeuw
584786eb9f Temp disable danger. 2018-01-25 17:00:51 +01:00
Michael Teeuw
d803d9eaf9 Import 'includes' from lodash. 2018-01-25 16:39:49 +01:00
Michael Teeuw
bad6575d83 Add Danger.js 2018-01-25 16:24:05 +01:00
Michael Teeuw
fbcb7ae836 Update README.md 2018-01-23 19:10:37 +01:00
Michael Teeuw
2c1a1b10c8 Merge pull request #1147 from E3V3A/patch-2
Update README.md
2018-01-23 09:51:15 +01:00
E:V:A
155fb16a8a Update README.md
Updated README by reformatting and restructuring.
- Added, Clarified and corrected grammatics and wrong info 
- Moved/Removed some redundant parts
2018-01-23 10:44:55 +02:00
Michael Teeuw
b1ab2ce96a Merge pull request #1146 from cederstrom/put-article-in-front-of-modules
Put article iframe in front of modules
2018-01-21 11:33:22 +01:00
Andreas Cederström
f299ba6218 Put article ifram in front of modules
Before this change the article was brought up in its ifram in fullscreen and you could still see the other modules in front of it
2018-01-20 23:46:58 +01:00
Michael Teeuw
d167ad1923 Merge pull request #1136 from E3V3A/patch-2
Update README
2018-01-19 09:15:54 +01:00
E:V:A
93626e8154 Fixed bullet points markup for lint 2018-01-19 09:13:59 +02:00
Michael Teeuw
d5040c091a Merge pull request #1143 from shbatm/bug-fix
Fix for #1140 - sendNotification module errors after #1116
2018-01-18 13:35:47 +01:00
shbatm
868daef0f0 Fix for #1140 - sendNotification module errors after #1116 2018-01-17 09:49:17 -06:00
Michael Teeuw
dc8e85e7f2 Merge pull request #1139 from amcolash/patch-1
Change English translation to "In 2 days"
2018-01-16 09:20:57 +01:00
Michael Teeuw
22d32d7ca3 Merge pull request #1141 from ConnorChristie/old-dom-event-fix
Fix to emit DOM_OBJECTS_CREATED event after module DOMs have actually loaded
2018-01-16 09:20:39 +01:00
Connor Christie
2d500f8074 Fix to emit DOM_OBJECTS_CREATED event after module DOMs have actually loaded 2018-01-14 22:03:09 -06:00
Andrew McOlash
452cdc17c6 Change English translation to "In 2 days" 2018-01-14 15:52:15 -06:00
E:V:A
bcbfee0321 Update README
Updated README to warn about long installation time as discussed in #1124.
2018-01-12 10:00:35 +02:00
Michael Teeuw
dab2e7ede3 Merge pull request #1127 from roramirez/set-version-test-7-node
Set only 7 version of node to run tests in Travis CI
2018-01-08 11:26:25 +01:00
Michael Teeuw
d91acb8352 Merge pull request #1126 from d-Rickyy-b/patch-1
Fix typo in newsfeed documentation
2018-01-08 11:23:49 +01:00
Rodrigo Ramírez Norambuena
373dd8058e Set only 7 version of node to run tests in Travis CI 2018-01-07 01:03:00 -03:00
Rico
de6310e52a Fix typo
A little typo which lead to poor formatting
2018-01-07 03:30:01 +01:00
Michael Teeuw
8c297a4a4c Merge pull request #1123 from E3V3A/patch-1
Added general advice
2018-01-06 19:49:44 +01:00
E:V:A
4eb5c817bc fixed typos 2018-01-06 20:19:43 +02:00
E:V:A
07e4b26b9e Added general advice 2018-01-06 19:57:46 +02:00
Michael Teeuw
b63aa62985 Merge pull request #1121 from henrikra/master
Add basic typescript types for module
2018-01-06 14:29:52 +01:00
Henrik Raitasola
ca701c0580 Merge branch 'master' of github.com:henrikra/MagicMirror 2018-01-06 15:26:51 +02:00
Henrik Raitasola
e37043a6a8 Solve conflict 2018-01-06 15:26:38 +02:00
Michael Teeuw
7c26975d14 Merge branch 'develop' into master 2018-01-06 14:16:21 +01:00
Henrik Raitasola
47f8a43637 Add changelog 2018-01-06 15:10:10 +02:00
Henrik Raitasola
1238c0cefe Rename file to be more explicit 2018-01-06 15:06:58 +02:00
Henrik Raitasola
4c35fda045 Add most used module properties 2018-01-06 15:04:54 +02:00
Henrik Raitasola
780124c2f5 Add basic types to get started 2018-01-06 14:51:32 +02:00
Michael Teeuw
38e0af41ce Merge pull request #1116 from ConnorChristie/async-dom
DOM creation notifications in cases of async template rendering
2018-01-04 21:51:13 +01:00
Connor Christie
601e99eec0 Merge branch 'develop' into async-dom 2018-01-02 18:10:07 -06:00
Connor Christie
745a2adee7 Merge branch 'develop' into async-dom 2018-01-02 18:08:55 -06:00
Connor Christie
9e83234df1 Update changelog 2018-01-02 18:06:21 -06:00
Connor Christie
e45eeadf7d Merge remote-tracking branch 'origin/master' into async-dom 2018-01-02 18:04:03 -06:00
Michael Teeuw
9a778cea6b Merge v2.2.2 changes. 2018-01-02 18:46:41 +01:00
Michael Teeuw
20823bfc87 Add missing package-lock.json. 2018-01-02 18:41:33 +01:00
Connor Christie
be3d703692 Fix linter errors 2018-01-01 10:55:39 -06:00
Connor Christie
e2df5739f0 Update module docs 2018-01-01 10:40:52 -06:00
Connor Christie
7bb11d6436 Add documentation regarding updates 2018-01-01 10:38:00 -06:00
Connor Christie
80b84212cc Add notification for module dom creation with async support 2018-01-01 10:23:15 -06:00
Connor Christie
4a1bee769b Add true module dom creation events 2018-01-01 09:42:34 -06:00
Michael Teeuw
de99a7aeaf Merge 2.2.1 changes. 2018-01-01 14:23:38 +01:00
Michael Teeuw
538a2acbf5 Fix linting errors. 2018-01-01 13:38:07 +01:00
Michael Teeuw
8d0e453666 Preparation for v2.3.0 release. 2018-01-01 12:59:28 +01:00
67 changed files with 8119 additions and 661 deletions

View File

@@ -12,5 +12,11 @@
"browser": true,
"node": true,
"es6": true
}
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": true
}
}
}

View File

@@ -1,10 +1,8 @@
language: node_js
node_js:
- "8"
- "7"
- "6"
- "5.1"
before_script:
- yarn danger ci
- npm install grunt-cli -g
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"

View File

@@ -2,11 +2,54 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.3.1] - 2018-04-01
### Fixed
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243)
## [2.3.0] - 2018-04-01
### 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
- Add advanced filtering to the excludedEvents configuration of the default calendar module
- New currentweather module config option: `showFeelsLike`: Shows how it actually feels like. (wind chill or heat index)
- New currentweather module config option: `useKMPHwind`: adds an option to see wind speed in Kmph instead of just m/s or Beaufort.
- Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds.
### 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.
- Yarn can be used now as an installation tool
- Changed Electron dependency to v1.7.13.
### Fixed
- News article in fullscreen (iframe) is now shown in front of modules.
- Forecast respects maxNumberOfDays regardless of endpoint.
- Fix exception on translation of objects.
## [2.2.2] - 2018-01-02
### Added
- Add missing `package-lock.json`.
### Changed
- Changed Electron dependency to v1.7.10.
## [2.2.1] - 2018-01-01
### Fixed
- Fixed linting errors.
## [2.2.0] - 2018-01-01

177
README.md
View File

@@ -16,114 +16,123 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
## Table Of Contents
- [Usage](#usage)
- [Installation](#installation)
- [Raspberry Pi](#raspberrypi)
- [General](#general)
- [Server Only](#server-only)
- [Client Only](#client-only)
- [Docker](#docker)
- [Configuration](#configuration)
- [Modules](#modules)
- [Updating](#updating)
- [Known Issues](#known-issues)
- [Community](#community)
- [Contributing Guidelines](#contributing-guidelines)
- [Manifesto](#manifesto)
## Usage
## Installation
### Raspberry Pi Support
Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2 & 3. The Raspberry Pi 1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself.
### Raspberry Pi
### 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²:
````
```bash
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`
3. Enter the repository: `cd ~/MagicMirror`
4. Install and run the app: `npm install && npm start`
3. Enter the repository: `cd MagicMirror/`
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
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
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:
```bash
docker run -d \
--publish 80:8080 \
--restart always \
--volume ~/magic_mirror/config:/opt/magic_mirror/config \
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
--name magic_mirror \
bastilimbach/docker-magicmirror
--publish 80:8080 \
--restart always \
--volume ~/magic_mirror/config:/opt/magic_mirror/config \
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
--name magic_mirror \
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
1. Duplicate `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.
### Raspberry Specific
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:
| **Option** | **Description** |
| --- | --- |
| `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`. |
| `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) |
| `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"]`, 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`|
| `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`. |
@@ -156,12 +165,20 @@ The following modules are installed by default.
- [**Hello World**](modules/default/helloworld)
- [**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)!
## Known issues
- Electron seems to have some issues on certain Raspberry Pi 2's. See [#145](https://github.com/MichMich/MagicMirror/issues/145).
- MagicMirror² (Electron) sometimes quits without an error after an extended period of use. See [#150](https://github.com/MichMich/MagicMirror/issues/150).
## 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.
## Community
@@ -180,6 +197,22 @@ Please keep the following in mind:
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">
<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>

17
dangerfile.js Normal file
View 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.")
}
}

7
fonts/yarn.lock Normal file
View File

@@ -0,0 +1,7 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
roboto-fontface@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/roboto-fontface/-/roboto-fontface-0.8.0.tgz#031a83c8f79932801a57d83bf743f37250163499"

View File

@@ -42,7 +42,7 @@ sudo apt-get update || echo -e "\e[91mUpdate failed, carrying on installation ..
# Installing helper tools
echo -e "\e[96mInstalling helper tools ...\e[90m"
sudo apt-get install curl wget git build-essential unzip || exit
sudo apt-get --assume-yes install curl wget git build-essential unzip || exit
# Check if we need to install or upgrade Node.js.
echo -e "\e[96mCheck current Node installation ...\e[0m"
@@ -82,7 +82,7 @@ if $NODE_INSTALL; then
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
NODE_STABLE_BRANCH="6.x"
NODE_STABLE_BRANCH="9.x"
curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
sudo apt-get install -y nodejs
echo -e "\e[92mNode.js installation Done!\e[0m"
@@ -101,7 +101,7 @@ if [ -d "$HOME/MagicMirror" ] ; then
fi
echo -e "\e[96mCloning MagicMirror ...\e[90m"
if git clone https://github.com/MichMich/MagicMirror.git; then
if git clone --depth=1 https://github.com/MichMich/MagicMirror.git; then
echo -e "\e[92mCloning MagicMirror Done!\e[0m"
else
echo -e "\e[91mUnable to clone MagicMirror."
@@ -149,7 +149,7 @@ else
fi
# Use pm2 control like a service MagicMirror
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/n)?" choice
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/N)?" choice
if [[ $choice =~ ^[Yy]$ ]]; then
sudo npm install -g pm2
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"

View File

@@ -92,7 +92,4 @@ function cloneObject(obj) {
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = Class;
module.exports._test = {
cloneObject: cloneObject
}
}

View File

@@ -1,5 +1,5 @@
/* global Log, Loader, Module, config, defaults */
/* jshint -W020 */
/* jshint -W020, esversion: 6 */
/* Magic Mirror
* Main System
@@ -19,42 +19,49 @@ var MM = (function() {
* are configured for a specific position.
*/
var createDomObjects = function() {
for (var m in modules) {
var module = modules[m];
var domCreationPromises = [];
if (typeof module.data.position === "string") {
var wrapper = selectWrapper(module.data.position);
var dom = document.createElement("div");
dom.id = module.identifier;
dom.className = module.name;
if (typeof module.data.classes === "string") {
dom.className = "module " + dom.className + " " + module.data.classes;
}
dom.opacity = 0;
wrapper.appendChild(dom);
if (typeof module.data.header !== "undefined" && module.data.header !== "") {
var moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.data.header;
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader);
}
var moduleContent = document.createElement("div");
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
updateDom(module, 0);
modules.forEach(module => {
if (typeof module.data.position !== "string") {
return;
}
}
var wrapper = selectWrapper(module.data.position);
var dom = document.createElement("div");
dom.id = module.identifier;
dom.className = module.name;
if (typeof module.data.classes === "string") {
dom.className = "module " + dom.className + " " + module.data.classes;
}
dom.opacity = 0;
wrapper.appendChild(dom);
if (typeof module.data.header !== "undefined" && module.data.header !== "") {
var moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.data.header;
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader);
}
var moduleContent = document.createElement("div");
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
var domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise);
domCreationPromise.then(() => {
sendNotification("MODULE_DOM_CREATED", null, null, module);
}).catch(Log.error);
});
updateWrapperStates();
sendNotification("DOM_OBJECTS_CREATED");
Promise.all(domCreationPromises).then(() => {
sendNotification("DOM_OBJECTS_CREATED");
});
};
/* selectWrapper(position)
@@ -79,11 +86,12 @@ var MM = (function() {
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
* argument sender Module - The module that sent the notification.
* 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) {
var module = modules[m];
if (module !== sender) {
if (module !== sender && (!sendTo || module === sendTo)) {
module.notificationReceived(notification, payload, sender);
}
}
@@ -94,19 +102,53 @@ var MM = (function() {
*
* argument module Module - The module that needs an update.
* 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 newContent = module.getDom();
var newHeader = module.getHeader();
return new Promise((resolve) => {
var newContentPromise = module.getDom();
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)) {
resolve();
return;
}
if (!speed) {
updateModuleContent(module, newHeader, newContent);
resolve();
return;
}
@@ -115,16 +157,16 @@ var MM = (function() {
if (!module.hidden) {
showModule(module, speed / 2);
}
resolve();
});
} else {
updateModuleContent(module, newHeader, newContent);
}
});
};
/* moduleNeedsUpdate(module, newContent)
* Check if the content has changed.
*
* 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.
*
* return bool - Does the module need an update?
@@ -152,6 +194,7 @@ var MM = (function() {
* Update the content of a module on screen.
*
* 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.
*/
var updateModuleContent = function(module, newHeader, newContent) {

View File

@@ -78,34 +78,33 @@ var Module = Class.extend({
* This method can to be subclassed if the module wants to display info on the mirror.
* 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 () {
var div = document.createElement("div");
var template = this.getTemplate();
var templateData = this.getTemplateData();
return new Promise((resolve) => {
var div = document.createElement("div");
var template = this.getTemplate();
var templateData = this.getTemplateData();
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
// the template is a filename
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (err) {
Log.error(err)
}
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
// the template is a filename
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (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;
});
} else {
// the template is a template string.
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
}
div.innerHTML = res;
return div;
resolve(div);
});
} else {
// the template is a template string.
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
resolve(div);
}
});
},
/* getHeader()
@@ -477,11 +476,3 @@ Module.register = function (name, moduleDefinition) {
Log.log("Module registered: " + name);
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
}
}

View File

@@ -126,6 +126,9 @@ var Translator = (function() {
// variables: {timeToWait: "2 hours", work: "painting"}
// to: "Please wait for 2 hours before continuing with painting."
function createStringFromTemplate(template, variables) {
if(Object.prototype.toString.call(template) !== "[object String]") {
return template;
}
if(variables.fallback && !template.match(new RegExp("\{.+\}"))) {
template = variables.fallback;
}
@@ -156,11 +159,12 @@ var Translator = (function() {
return key;
},
/* load(module, file, callback)
/* load(module, file, isFallback, callback)
* Load a translation file (json) and remember the data.
*
* argument module Module - The module to load the translation file for.
* 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.
*/
load: function(module, file, isFallback, callback) {
@@ -216,10 +220,12 @@ var Translator = (function() {
// defined translation after the following line.
for (var first in translations) {break;}
Log.log("Loading core translation fallback file: " + translations[first]);
loadJSON(translations[first], function(translations) {
self.coreTranslationsFallback = translations;
});
if (first) {
Log.log("Loading core translation fallback file: " + translations[first]);
loadJSON(translations[first], function(translations) {
self.coreTranslationsFallback = translations;
});
}
},
};
})();

31
module-types.ts Normal file
View 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,
};

View File

@@ -2,6 +2,42 @@
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
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/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:
````javascript
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
After the module is initialized, the module instance has a few available module properties:
#### `this.name`
**String**
| Instance Property | Type | Description |
|:----------------- |:---- |:----------- |
| `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`
**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:
The `this.data` data object contain the follwoing metadata:
- `data.classes` - The classes which are added to the module dom wrapper.
- `data.file` - The filename of the core module file.
- `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.
- `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)`

View File

@@ -60,5 +60,5 @@ self.sendNotification("SHOW_ALERT", {});
| `timer` (optional) | How long the alert should stay visible in ms. <br> **Important:** If you do not use the `timer`, it is your duty to hide the alert by using `self.sendNotification("HIDE_ALERT");`! <br><br>**Possible values:** `int` `float` <br> **Default value:** `none`
## Open Source Licenses
###[NotificationStyles](https://github.com/codrops/NotificationStyles)
### [NotificationStyles](https://github.com/codrops/NotificationStyles)
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.

View File

@@ -46,7 +46,7 @@ The following properties can be configured:
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7`
| `broadcastEvents` | If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: `CALENDAR_EVENTS`. The event objects are stored in an array and contain the following fields: `title`, `startDate`, `endDate`, `fullDayEvent`, `location` and `geo`. <br><br> **Possible values:** `true`, `false` <br><br> **Default value:** `true`
| `hidePrivate` | Hides private calendar events. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br> **Example:** `['Birthday', 'Hide This Event']` <br> **Default value:** `[]`
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br>Additionally advanced filter objects can be passed in. Below is the configuration for the advance filtering object.<br>**Required**<br>`filterBy` - string used to determine if filter is applied.<br>**Optional**<br>`until` - Time before an event to display it Ex: [`'3 days'`, `'2 months'`, `'1 week'`]<br>`caseSensitive` - By default, excludedEvents are case insensitive, set this to true to enforce case sensitivity<br><br> **Example:** `['Birthday', 'Hide This Event', {filterBy: 'Payment', until: '6 days', caseSensitive: true}]` <br> **Default value:** `[]`
### Calendar configuration

View File

@@ -113,11 +113,38 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
title = event.description;
}
var excluded = false;
var excluded = false,
dateFilter = null;
for (var f in excludedEvents) {
var filter = excludedEvents[f];
if (title.toLowerCase().includes(filter.toLowerCase())) {
excluded = true;
var filter = excludedEvents[f],
testTitle = title.toLowerCase(),
until = null;
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (testTitle.includes(filter)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
@@ -137,6 +164,11 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
for (var d in dates) {
startDate = moment(new Date(dates[d]));
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (timeFilterApplies(now, endDate, dateFilter)) {
continue;
}
if (endDate.format("x") > now) {
newEvents.push({
title: title,
@@ -171,6 +203,10 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
continue;
}
if (timeFilterApplies(now, endDate, dateFilter)) {
continue;
}
// Every thing is good. Add it to the list.
newEvents.push({
@@ -236,6 +272,28 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
return false;
};
/* timeFilterApplies()
* Determines if the user defined time filter should apply
*
* argument now Date - Date object using previously created object for consistency
* argument endDate Moment - Moment object representing the event end date
* argument filter string - The time to subtract from the end date to determine if an event should be shown
*
* return bool - The event should be filtered out
*/
var timeFilterApplies = function(now, endDate, filter) {
if (filter) {
var until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice("-1") === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
};
/* public methods */
/* startFetch()

View File

@@ -108,7 +108,7 @@ vows.describe('node-ical').addBatch({
assert.equal(topic.end.getFullYear(), 1998);
assert.equal(topic.end.getUTCMonth(), 2);
assert.equal(topic.end.getUTCDate(), 15);
assert.equal(topic.end.getUTCHours(), 00);
assert.equal(topic.end.getUTCHours(), 0);
assert.equal(topic.end.getUTCMinutes(), 30);
}
}
@@ -146,7 +146,7 @@ vows.describe('node-ical').addBatch({
}
, 'has a start datetime' : function(topic) {
assert.equal(topic.start.getFullYear(), 2011);
assert.equal(topic.start.getMonth(), 09);
assert.equal(topic.start.getMonth(), 9);
assert.equal(topic.start.getDate(), 11);
}
@@ -192,7 +192,7 @@ vows.describe('node-ical').addBatch({
}
, 'has a start' : function(topic){
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];
}
, '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' :{
@@ -249,7 +249,7 @@ vows.describe('node-ical').addBatch({
},
'task completed': function(task){
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.getMonth(), 3);
assert.equal(topic.end.getUTCHours(), 19);
assert.equal(topic.end.getUTCMinutes(), 00);
assert.equal(topic.end.getUTCMinutes(), 0);
}
}
},

View File

@@ -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.
| `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`
| `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

View File

@@ -32,7 +32,11 @@ Module.register("compliments", {
},
updateInterval: 30000,
remoteFile: null,
fadeSpeed: 4000
fadeSpeed: 4000,
morningStartTime: 3,
morningEndTime: 12,
afternoonStartTime: 12,
afternoonEndTime: 17
},
// Set currentweather from module
@@ -49,14 +53,15 @@ Module.register("compliments", {
this.lastComplimentIndex = -1;
var self = this;
if (this.config.remoteFile != null) {
this.complimentFile((response) => {
this.config.compliments = JSON.parse(response);
self.updateDom();
});
}
// Schedule update timer.
var self = this;
setInterval(function() {
self.updateDom(self.config.fadeSpeed);
}, this.config.updateInterval);
@@ -98,9 +103,9 @@ Module.register("compliments", {
var hour = moment().hour();
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);
} 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);
} else if(this.config.compliments.hasOwnProperty("evening")) {
compliments = this.config.compliments.evening.slice(0);

View File

@@ -43,7 +43,9 @@ 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`
| `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`
| `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, sunrise time and feels like. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `showFeelsLike` | Shows the Feels like temperature weather. <br><br> **Possible values:**`true` or `false`<br>**Default value:** `true`
| `useKMPHWind` | Uses KMPH as units for windspeed. <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`
| `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:** `.`

View File

@@ -23,12 +23,14 @@ Module.register("currentweather",{
showWindDirection: true,
showWindDirectionAsArrow: false,
useBeaufort: true,
useKMPHwind: false,
lang: config.language,
decimalSymbol: ".",
showHumidity: false,
degreeLabel: false,
showIndoorTemperature: false,
showIndoorHumidity: false,
showFeelsLike: true,
initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500,
@@ -105,7 +107,7 @@ Module.register("currentweather",{
this.indoorTemperature = null;
this.indoorHumidity = null;
this.weatherType = null;
this.feelsLike = null;
this.loaded = false;
this.scheduleUpdate(this.config.initialLoadDelay);
@@ -242,6 +244,19 @@ Module.register("currentweather",{
}
wrapper.appendChild(large);
if (this.config.showFeelsLike && this.config.onlyTemp === false){
var small = document.createElement("div");
small.className = "normal medium";
var feelsLike = document.createElement("span");
feelsLike.className = "dimmed";
feelsLike.innerHTML = "Feels " + this.feelsLike + "&deg;" + degreeLabel;
small.appendChild(feelsLike);
wrapper.appendChild(small);
}
return wrapper;
},
@@ -365,13 +380,71 @@ Module.register("currentweather",{
this.humidity = parseFloat(data.main.humidity);
this.temperature = this.roundValue(data.main.temp);
this.feelsLike = 0;
if (this.config.useBeaufort){
this.windSpeed = this.ms2Beaufort(this.roundValue(data.wind.speed));
} else if (this.config.useKMPHwind) {
this.windSpeed = parseFloat((data.wind.speed * 60 * 60) / 1000).toFixed(0);
} else {
this.windSpeed = parseFloat(data.wind.speed).toFixed(0);
}
// ONLY WORKS IF TEMP IN C //
var windInMph = parseFloat(data.wind.speed * 2.23694);
var tempInF = 0;
switch (this.config.units){
case "metric": tempInF = 1.8 * this.temperature + 32;
break;
case "imperial": tempInF = this.temperature;
break;
case "default":
var tc = this.temperature - 273.15;
tempInF = 1.8 * tc + 32;
break;
}
if (windInMph > 3 && tempInF < 50){
// windchill
var windchillinF = Math.round(35.74+0.6215*tempInF-35.75*Math.pow(windInMph,0.16)+0.4275*tempInF*Math.pow(windInMph,0.16));
var windChillInC = (windchillinF - 32) * (5/9);
// this.feelsLike = windChillInC.toFixed(0);
switch (this.config.units){
case "metric": this.feelsLike = windChillInC.toFixed(0);
break;
case "imperial": this.feelsLike = windChillInF.toFixed(0);
break;
case "default":
var tc = windChillInC - 273.15;
this.feelsLike = tc.toFixed(0);
break;
}
} else if (tempInF > 80 && this.humidity > 40){
// heat index
var Hindex = -42.379 + 2.04901523*tempInF + 10.14333127*this.humidity
- 0.22475541*tempInF*this.humidity - 6.83783*Math.pow(10,-3)*tempInF*tempInF
- 5.481717*Math.pow(10,-2)*this.humidity*this.humidity
+ 1.22874*Math.pow(10,-3)*tempInF*tempInF*this.humidity
+ 8.5282*Math.pow(10,-4)*tempInF*this.humidity*this.humidity
- 1.99*Math.pow(10,-6)*tempInF*tempInF*this.humidity*this.humidity;
switch (this.config.units){
case "metric": this.feelsLike = Hindex.toFixed(0);
break;
case "imperial": this.feelsLike = parseFloat(Hindex * 1.8 + 32).toFixed(0);
break;
case "default":
var tc = Hindex - 273.15;
this.feelsLike = tc.toFixed(0);
break;
}
} else {
this.feelsLike = parseFloat(this.temperature).toFixed(0);
}
this.windDirection = this.deg2Cardinal(data.wind.deg);
this.windDeg = data.wind.deg;
this.weatherType = this.config.iconTable[data.weather[0].icon];
@@ -497,4 +570,5 @@ Module.register("currentweather",{
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
}
});

View File

@@ -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_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.
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)
| `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`
| `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)
| `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',...]`
| `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',...]`
| `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:

View File

@@ -45,7 +45,7 @@ var Fetcher = function(url, reloadInterval, encoding) {
var title = item.title;
var description = item.description || item.summary || item.content || "";
var pubdate = item.pubdate || item.published || item.updated;
var pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
var url = item.url || item.link || "";
if (title && pubdate) {

View File

@@ -36,7 +36,8 @@ Module.register("newsfeed",{
removeEndTags: "",
startTags: [],
endTags: [],
prohibitedWords: []
prohibitedWords: [],
scrollLength: 500
},
// Define required scripts.
@@ -62,6 +63,7 @@ Module.register("newsfeed",{
this.newsItems = [];
this.loaded = false;
this.activeItem = 0;
this.scrollPosition = 0;
this.registerFeeds();
@@ -171,7 +173,6 @@ Module.register("newsfeed",{
var description = document.createElement("div");
description.className = "small light" + (!this.config.wrapDescription ? " no-wrap" : "");
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);
wrapper.appendChild(description);
}
@@ -180,12 +181,14 @@ Module.register("newsfeed",{
var fullArticle = document.createElement("iframe");
fullArticle.className = "";
fullArticle.style.width = "100%";
// very large height value to allow scrolling
fullArticle.height = "10000";
fullArticle.style.height = "10000";
fullArticle.style.top = "0";
fullArticle.style.left = "0";
fullArticle.style.position = "fixed";
fullArticle.height = window.innerHeight;
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);
}
@@ -322,6 +325,10 @@ Module.register("newsfeed",{
resetDescrOrFullArticleAndTimer: function() {
this.config.showDescription = 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){
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
else if(notification == "ARTICLE_MORE_DETAILS"){
this.config.showDescription = !this.config.showDescription;
this.config.showFullArticle = !this.config.showDescription;
clearInterval(timer);
timer = null;
Log.info(this.name + " - showing " + this.config.showDescription ? "article description" : "full article");
this.updateDom(100);
// 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.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);
timer = null;
Log.info(this.name + " - showing " + this.config.showDescription ? "article description" : "full article");
this.updateDom(100);
}
} else if(notification == "ARTICLE_LESS_DETAILS"){
this.resetDescrOrFullArticleAndTimer();
Log.info(this.name + " - showing only article titles again");

View File

@@ -58,16 +58,19 @@ Module.register("updatenotification", {
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var subtextHtml = this.translate("UPDATE_INFO")
.replace("COMMIT_COUNT", this.status.behind + " " + ((this.status.behind == 1) ? "commit" : "commits"))
.replace("BRANCH_NAME", this.status.current);
var subtextHtml = this.translate("UPDATE_INFO", {
COMMIT_COUNT: this.status.behind + " " + ((this.status.behind == 1) ? "commit" : "commits"),
BRANCH_NAME: this.status.current
});
var text = document.createElement("span");
if (this.status.module == "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(subtextHtml);
} 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);

View File

@@ -259,7 +259,6 @@ Module.register("weatherforecast",{
if (self.config.forecastEndpoint == "forecast/daily") {
self.config.forecastEndpoint = "forecast";
self.config.maxNumberOfDays = self.config.maxNumberOfDays * 8;
Log.warn(self.name + ": Your AppID does not support long term forecasts. Switching to fallback endpoint.");
}
@@ -298,12 +297,6 @@ Module.register("weatherforecast",{
params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang;
/*
* Submit a specific number of days to forecast, between 1 to 16 days.
* The OpenWeatherMap API properly handles values outside of the 1 - 16 range and returns 7 days by default.
* This is simply being pedantic and doing it ourselves.
*/
params += "&cnt=" + (((this.config.maxNumberOfDays < 1) || (this.config.maxNumberOfDays > 16)) ? 7 * 8 : this.config.maxNumberOfDays);
params += "&APPID=" + this.config.appid;
return params;
@@ -354,6 +347,11 @@ Module.register("weatherforecast",{
this.forecast.push(forecastData);
lastDay = day;
// Stop processing when maxNumberOfDays is reached
if (this.forecast.length === this.config.maxNumberOfDays) {
break;
}
} else {
//Log.log("Compare max: ", forecast.temp.max, parseFloat(forecastData.maxTemp));
forecastData.maxTemp = forecast.temp.max > parseFloat(forecastData.maxTemp) ? this.roundValue(forecast.temp.max) : forecastData.maxTemp;

1506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,18 @@
{
"name": "magicmirror",
"version": "2.2.1",
"version": "2.3.1",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
"start": "sh run-start.sh",
"install": "cd vendor && npm install",
"install-fonts": "cd fonts && npm install",
"postinstall": "sh installers/postinstall/postinstall.sh && npm run install-fonts",
"install": "cd vendor && yon install",
"install-fonts": "cd fonts && yon install",
"postinstall": "sh installers/postinstall/postinstall.sh && yon run install-fonts",
"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: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": {
"type": "git",
@@ -36,6 +37,7 @@
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"current-week-number": "^1.0.7",
"danger": "^3.1.3",
"grunt": "latest",
"grunt-eslint": "latest",
"grunt-jsonlint": "latest",
@@ -43,18 +45,20 @@
"grunt-stylelint": "latest",
"grunt-yamllint": "latest",
"http-auth": "^3.2.3",
"jsdom": "^11.6.2",
"jshint": "^2.9.5",
"mocha": "^4.1.0",
"mocha-each": "^1.1.0",
"spectron": "3.7.x",
"stylelint": "^8.4.0",
"stylelint-config-standard": "latest",
"time-grunt": "latest"
"time-grunt": "latest",
"yarn-or-npm": "^2.0.4"
},
"dependencies": {
"body-parser": "^1.18.2",
"colors": "^1.1.2",
"electron": "1.4.15",
"electron": "^1.4.15",
"express": "^4.16.2",
"express-ipfilter": "0.3.1",
"feedme": "latest",

View File

@@ -14,8 +14,6 @@ var path = require("path");
var fs = require("fs");
var Utils = require(__dirname + "/../../js/utils.js");
if (process.env.NODE_ENV == "test") { return 0 };
/* getConfigFile()
* Return string with path of configuration file
* Check if set by enviroment variable MM_CONFIG_FILE
@@ -30,37 +28,43 @@ function getConfigFile() {
return configFileName;
}
var configFileName = getConfigFile();
// Check if file is present
if (fs.existsSync(configFileName) === false) {
console.error(Utils.colors.error("File not found: "), configFileName);
return;
}
// check permision
try {
fs.accessSync(configFileName, fs.F_OK);
} catch (e) {
console.log(Utils.colors.error(e));
return;
}
// Validate syntax of the configuration file.
// In case the there errors show messages and
// return
console.info(Utils.colors.info("Checking file... ", configFileName));
// I'm not sure if all ever is utf-8
fs.readFile(configFileName, "utf-8", function (err, data) {
if (err) { throw err; }
v.JSHINT(data); // Parser by jshint
if (v.JSHINT.errors.length == 0) {
console.log("Your configuration file doesn't contain syntax errors :)");
return true;
} else {
errors = v.JSHINT.data().errors;
for (idx in errors) {
error = errors[idx];
console.log("Line", error.line, "col", error.character, error.reason);
}
function checkConfigFile() {
var configFileName = getConfigFile();
// Check if file is present
if (fs.existsSync(configFileName) === false) {
console.error(Utils.colors.error("File not found: "), configFileName);
return;
}
});
// check permision
try {
fs.accessSync(configFileName, fs.F_OK);
} catch (e) {
console.log(Utils.colors.error(e));
return;
}
// Validate syntax of the configuration file.
// In case the there errors show messages and
// return
console.info(Utils.colors.info("Checking file... ", configFileName));
// I'm not sure if all ever is utf-8
fs.readFile(configFileName, "utf-8", function (err, data) {
if (err) { throw err; }
v.JSHINT(data); // Parser by jshint
if (v.JSHINT.errors.length == 0) {
console.log("Your configuration file doesn't contain syntax errors :)");
return true;
} else {
errors = v.JSHINT.data().errors;
for (idx in errors) {
error = errors[idx];
console.log("Line", error.line, "col", error.character, error.reason);
}
}
});
}
if (process.env.NODE_ENV !== "test") {
checkConfigFile();
};

View File

@@ -0,0 +1,13 @@
{
// Escaped
"FOO\"BAR": "Today",
/*
* The following lines
* represent cardinal directions
*/
"N": "N",
"E": "E",
"S": "S",
"W": "W"
}

View File

@@ -0,0 +1,32 @@
{
"LOADING": "Loading &hellip;",
"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."
}

View File

@@ -0,0 +1,32 @@
{
"LOADING": "Loading &hellip;",
"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."
}

View 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;
}
}
})
});
}
});
});

View 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);
});
});
});
});

View 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");
});
});

View 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);
};
});
});
});

View 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!");
});
});
});

View File

@@ -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);
});
});
*/
});
});

View File

@@ -1,20 +1,32 @@
var chai = require("chai");
var expect = chai.expect;
var classMM = require("../../../js/class.js"); // require for load module.js
var moduleMM = require("../../../js/module.js")
const chai = require("chai");
const expect = chai.expect;
const path = require("path");
const {JSDOM} = require("jsdom");
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() {
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() {
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() {
expect(moduleMM._test.cmpVersions("1.1", "1.0")).to.equal(1);
expect(cmp("1.1", "1.0")).to.equal(1);
});
});

View File

@@ -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));
}

View File

@@ -25,6 +25,6 @@
"NNW": "NNW",
"UPDATE_NOTIFICATION": "MagicMirror² update beskikbaar.",
"UPDATE_NOTIFICATION_MODULE": "Update beskikbaar vir MODULE_NAME module.",
"UPDATE_INFO": "Die huidige installasie is COMMIT_COUNT agter op die BRANCH_NAME branch."
"UPDATE_NOTIFICATION_MODULE": "Update beskikbaar vir {MODULE_NAME} module.",
"UPDATE_INFO": "Die huidige installasie is {COMMIT_COUNT} agter op die {BRANCH_NAME} branch."
}

View File

@@ -27,6 +27,6 @@
"NNW": "ССЗ",
"UPDATE_NOTIFICATION": "Налична актуализация за MagicMirror².",
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за MODULE_NAME модул.",
"UPDATE_INFO": "Текущата инсталация е изостанала с COMMIT_COUNT къмита на клон BRANCH_NAME."
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за {MODULE_NAME} модул.",
"UPDATE_INFO": "Текущата инсталация е изостанала с {COMMIT_COUNT} къмита на клон {BRANCH_NAME}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNO",
"UPDATE_NOTIFICATION": "MagicMirror² actualizació disponible.",
"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_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}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "SSZ",
"UPDATE_NOTIFICATION": "Dostupná aktualizace pro MagicMirror².",
"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_NOTIFICATION_MODULE": "Dostupná aktualizace pro modul {MODULE_NAME}.",
"UPDATE_INFO": "Současná instalace je na větvi {BRANCH_NAME} pozadu o {COMMIT_COUNT}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "GoGoGe",
"UPDATE_NOTIFICATION": "MagicMirror² mwy diweddar yn barod.",
"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_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}."
}

View File

@@ -26,6 +26,6 @@
"UPDATE_NOTIFICATION": "MagicMirror² opdatering tilgængelig.",
"UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for MODULE_NAME modulet.",
"UPDATE_INFO": "Den nuværende installation er COMMIT_COUNT bagud på BRANCH_NAME branch'en."
"UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for {MODULE_NAME} modulet.",
"UPDATE_INFO": "Den nuværende installation er {COMMIT_COUNT} bagud på {BRANCH_NAME} branch'en."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNW",
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das MODULE_NAME Modul verfügbar.",
"UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch."
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.",
"UPDATE_INFO": "Die aktuelle Installation ist {COMMIT_COUNT} hinter dem {BRANCH_NAME} branch."
}

View File

@@ -3,7 +3,7 @@
"TODAY": "Today",
"TOMORROW": "Tomorrow",
"DAYAFTERTOMORROW": "The day after tomorrow",
"DAYAFTERTOMORROW": "In 2 days",
"RUNNING": "Ends in",
"EMPTY": "No upcoming events.",
@@ -27,6 +27,6 @@
"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."
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
"UPDATE_INFO": "The current installation is {COMMIT_COUNT} behind on the {BRANCH_NAME} branch."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNO",
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo MODULE_NAME.",
"UPDATE_INFO": "Tu actual instalación está COMMIT_COUNT cambios detrás de la rama BRANCH_NAME."
"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}."
}

View File

@@ -25,6 +25,6 @@
"NNW": "Põhjaloe",
"UPDATE_NOTIFICATION": "MagicMirror²´le uuendus saadaval.",
"UPDATE_NOTIFICATION_MODULE": "Uuendus saadaval MODULE_NAME moodulile.",
"UPDATE_INFO": "Praegune paigaldus on COMMIT_COUNT tagapool BRANCH_NAME harul."
"UPDATE_NOTIFICATION_MODULE": "Uuendus saadaval {MODULE_NAME} moodulile.",
"UPDATE_INFO": "Praegune paigaldus on {COMMIT_COUNT} tagapool {BRANCH_NAME} harul."
}

View File

@@ -25,5 +25,5 @@
"NNW": "PPL",
"UPDATE_NOTIFICATION": "MagicMirror² päivitys saatavilla.",
"UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille MODULE_NAME."
"UPDATE_NOTIFICATION_MODULE": "Päivitys saatavilla moduulille {MODULE_NAME}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNO",
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
"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_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} ."
}

View File

@@ -25,6 +25,6 @@
"NNW": "ÉÉNy",
"UPDATE_NOTIFICATION": "MagicMirror² elérhető egy frissítés!",
"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_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ó."
}

View File

@@ -27,6 +27,6 @@
"NNW": "UBL",
"UPDATE_NOTIFICATION": "Memperbarui MagicMirror² tersedia.",
"UPDATE_NOTIFICATION_MODULE": "Memperbarui tersedia untuk modul MODULE_NAME.",
"UPDATE_INFO": "Instalasi saat ini tertinggal COMMIT_COUNT pada cabang BRANCH_NAME."
"UPDATE_NOTIFICATION_MODULE": "Memperbarui tersedia untuk modul {MODULE_NAME}.",
"UPDATE_INFO": "Instalasi saat ini tertinggal {COMMIT_COUNT} pada cabang {BRANCH_NAME}."
}

View File

@@ -25,6 +25,6 @@
"NNW": "NNV",
"UPDATE_NOTIFICATION": "MagicMirror² uppfærsla í boði.",
"UPDATE_NOTIFICATION_MODULE": "Uppfærsla í boði fyrir MODULE_NAME module.",
"UPDATE_INFO": "Núverandi kerfi er COMMIT_COUNT á eftir BRANCH_NAME branchinu."
"UPDATE_NOTIFICATION_MODULE": "Uppfærsla í boði fyrir {MODULE_NAME} module.",
"UPDATE_INFO": "Núverandi kerfi er {COMMIT_COUNT} á eftir {BRANCH_NAME} branchinu."
}

View File

@@ -25,6 +25,6 @@
"NNW": "북북서풍",
"UPDATE_NOTIFICATION": "새로운 MagicMirror² 업데이트가 있습니다.",
"UPDATE_NOTIFICATION_MODULE": "MODULE_NAME 모듈에서 사용 가능한 업데이트 입니다.",
"UPDATE_INFO": "설치할 COMMIT_COUNT 는 BRANCH_NAME 분기에 해당됩니다."
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} 모듈에서 사용 가능한 업데이트 입니다.",
"UPDATE_INFO": "설치할 {COMMIT_COUNT}{BRANCH_NAME} 분기에 해당됩니다."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNV",
"UPDATE_NOTIFICATION": "MagicMirror²-oppdatering er tilgjengelig.",
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen MODULE_NAME.",
"UPDATE_INFO": "Nåværende installasjon er COMMIT_COUNT bak BRANCH_NAME grenen."
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen {MODULE_NAME}.",
"UPDATE_INFO": "Nåværende installasjon er {COMMIT_COUNT} bak {BRANCH_NAME} grenen."
}

View File

@@ -25,6 +25,6 @@
"NNW": "NNW",
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor MODULE_NAME module.",
"UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch."
"UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor {MODULE_NAME} module.",
"UPDATE_INFO": "De huidige installatie loopt {COMMIT_COUNT} achter op de {BRANCH_NAME} branch."
}

View File

@@ -25,6 +25,6 @@
"NNW": "NNV",
"UPDATE_NOTIFICATION": "MagicMirror² oppdatering er tilgjengeleg.",
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengeleg for modulen MODULE_NAME.",
"UPDATE_INFO": "noverande installasjon er COMMIT_COUNT bak BRANCH_NAME greinen."
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengeleg for modulen {MODULE_NAME}.",
"UPDATE_INFO": "noverande installasjon er {COMMIT_COUNT} bak {BRANCH_NAME} greinen."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNW",
"UPDATE_NOTIFICATION": "Dostępna jest aktualizacja MagicMirror².",
"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_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}."
}

View File

@@ -26,6 +26,6 @@
"NNW": "NNO",
"UPDATE_NOTIFICATION": "Atualização do MagicMirror² 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_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}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNW",
"UPDATE_NOTIFICATION": "Un update este disponibil pentru MagicMirror².",
"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_NOTIFICATION_MODULE": "Un update este disponibil pentru modulul {MODULE_NAME}.",
"UPDATE_INFO": "Există {COMMIT_COUNT} commit-uri noi pe branch-ul {BRANCH_NAME}."
}

View File

@@ -27,6 +27,6 @@
"NNW": "ССЗ",
"UPDATE_NOTIFICATION": "Есть обновление для MagicMirror².",
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для MODULE_NAME модуля.",
"UPDATE_INFO": "Данная инсталляция позади BRANCH_NAME ветки на COMMIT_COUNT коммитов."
"UPDATE_NOTIFICATION_MODULE": "Есть обновление для {MODULE_NAME} модуля.",
"UPDATE_INFO": "Данная инсталляция позади {BRANCH_NAME} ветки на {COMMIT_COUNT} коммитов."
}

View File

@@ -27,6 +27,6 @@
"NNW": "NNV",
"UPDATE_NOTIFICATION": "MagicMirror² uppdatering finns tillgänglig.",
"UPDATE_NOTIFICATION_MODULE": "Uppdatering finns tillgänglig av MODULE_NAME modulen.",
"UPDATE_INFO": "Denna installation ligger COMMIT_COUNT steg bakom BRANCH_NAME grenen."
"UPDATE_NOTIFICATION_MODULE": "Uppdatering finns tillgänglig av {MODULE_NAME} modulen.",
"UPDATE_INFO": "Denna installation ligger {COMMIT_COUNT} steg bakom {BRANCH_NAME} grenen."
}

View File

@@ -25,6 +25,6 @@
"NNW": "北偏西风",
"UPDATE_NOTIFICATION": "MagicMirror² 有新的更新",
"UPDATE_NOTIFICATION_MODULE": "模块 MODULE_NAME 可更新",
"UPDATE_INFO": "当前已安装版本为 COMMIT_COUNT 落后于分支 BRANCH_NAME "
"UPDATE_NOTIFICATION_MODULE": "模块 {MODULE_NAME} 可更新",
"UPDATE_INFO": "当前已安装版本为 {COMMIT_COUNT} 落后于分支 {BRANCH_NAME} "
}

1007
vendor/yarn.lock vendored Normal file

File diff suppressed because it is too large Load Diff

4391
yarn.lock Normal file

File diff suppressed because it is too large Load Diff