Compare commits
731 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e6fb18df56 | ||
|
43ba13f3bc | ||
|
ba705f5563 | ||
|
34e188ec1f | ||
|
b0d97dd170 | ||
|
7caeae61f5 | ||
|
416ace4c86 | ||
|
979041ee91 | ||
|
f0939b8af5 | ||
|
d7a7002bdd | ||
|
d9601de075 | ||
|
ef570558cf | ||
|
db3e81408f | ||
|
e8771cdea8 | ||
|
057eab2173 | ||
|
e2a7024eeb | ||
|
4650986dfa | ||
|
868b5e4617 | ||
|
f12860c7b1 | ||
|
8f751812a6 | ||
|
07a5092eb3 | ||
|
29c9c92ba6 | ||
|
edfa327158 | ||
|
869a6e66cc | ||
|
36abbfc048 | ||
|
cfc3e6d2f4 | ||
|
e38dbee6a6 | ||
|
68a7c857c0 | ||
|
80eef2ab8c | ||
|
1d652aa746 | ||
|
b07c43aa36 | ||
|
3880c8dc2c | ||
|
c0ab2ac297 | ||
|
de684dcb63 | ||
|
29ef1db86b | ||
|
5dfd8a61be | ||
|
358e2b3ccf | ||
|
4203065a06 | ||
|
bc4e0190a0 | ||
|
dd7004cbc9 | ||
|
bdc5c8f620 | ||
|
02d36e22ee | ||
|
331e8c4aa6 | ||
|
d622277c11 | ||
|
8f781ea4ab | ||
|
ebc1e5bf12 | ||
|
ce9a61622e | ||
|
e4891e699f | ||
|
d8765578c8 | ||
|
3a034ecec8 | ||
|
a3dea45089 | ||
|
de99c8a5e4 | ||
|
d3b8dbeea0 | ||
|
cff2f64155 | ||
|
7b8de35405 | ||
|
02ae0df2cc | ||
|
b386cea69d | ||
|
758ffb75a9 | ||
|
78fbc7f392 | ||
|
9c58472576 | ||
|
f8c4afc228 | ||
|
b169d65619 | ||
|
9bf0d4f804 | ||
|
4443f57f8a | ||
|
ea5d8590d5 | ||
|
5d5feb4c71 | ||
|
feb5351ec3 | ||
|
7630c25ef3 | ||
|
24238094e5 | ||
|
7cc9a03db8 | ||
|
2b2e8508d9 | ||
|
a70716f225 | ||
|
d9fcc46994 | ||
|
2d8acec6f0 | ||
|
cd06d8c63a | ||
|
a06ca55107 | ||
|
9686a9ba77 | ||
|
f7f4043ccd | ||
|
b7b55173a6 | ||
|
954253c7e2 | ||
|
cbe4d2cd7f | ||
|
40101129b5 | ||
|
4bb32c6d09 | ||
|
6e09ceeda6 | ||
|
d6a6a53623 | ||
|
4a97052708 | ||
|
3a4902ad4a | ||
|
77d14bc218 | ||
|
1d2a39a855 | ||
|
98b53b6b3d | ||
|
0148d8beaf | ||
|
5bfd84d3be | ||
|
351eb95feb | ||
|
56788f0933 | ||
|
017a376616 | ||
|
c5888cec66 | ||
|
3d5ad29eac | ||
|
c608636b7a | ||
|
1a97107b2d | ||
|
5ca3fbeaea | ||
|
44896db668 | ||
|
12efb87a23 | ||
|
9181be86ba | ||
|
053b01e036 | ||
|
86041d0968 | ||
|
bd87f63e91 | ||
|
b79b49e8f3 | ||
|
a0dde39d97 | ||
|
2e03868021 | ||
|
29384c2ba3 | ||
|
320743ab8d | ||
|
399e171083 | ||
|
184164b677 | ||
|
6d9675a299 | ||
|
91e8ce62d4 | ||
|
06e641015f | ||
|
1c83059482 | ||
|
90b24d824a | ||
|
62457d0e48 | ||
|
90c96f7479 | ||
|
f87adebe41 | ||
|
8f24cc8d13 | ||
|
992802d196 | ||
|
8546d6730c | ||
|
0092289105 | ||
|
7c3923ad00 | ||
|
ef82039401 | ||
|
b01b9758e0 | ||
|
88b00f689b | ||
|
0a340d5d57 | ||
|
50545a83b8 | ||
|
4a57ff40d8 | ||
|
0238455a5a | ||
|
a53e963001 | ||
|
984608e23f | ||
|
f680c83d2d | ||
|
a79e51c76f | ||
|
2dfb349609 | ||
|
766f21b525 | ||
|
733dfa1467 | ||
|
63aa840b55 | ||
|
8b2d544576 | ||
|
d6046d2422 | ||
|
409939360f | ||
|
1d21f39fbc | ||
|
a477140a4b | ||
|
1bbf2d8ce6 | ||
|
40a65eec51 | ||
|
8431ebf2e8 | ||
|
bdcc0c5373 | ||
|
9cbf331533 | ||
|
77640714cc | ||
|
9457d95c3f | ||
|
c2ff949f2d | ||
|
ba8685a122 | ||
|
55464ed0dd | ||
|
fdf3691c87 | ||
|
aa6699cf3e | ||
|
b79b48baac | ||
|
5d22dbd99e | ||
|
4686bb5584 | ||
|
1f62b8f0b6 | ||
|
5759ed3728 | ||
|
827fbfb78f | ||
|
dc363de610 | ||
|
b9d6a235e3 | ||
|
ebc57fe494 | ||
|
e224ec4ae0 | ||
|
a5da347177 | ||
|
a257b15f86 | ||
|
a70cc53d82 | ||
|
1df2de9202 | ||
|
9e394ea349 | ||
|
b55685d610 | ||
|
2156aac046 | ||
|
6914465e3d | ||
|
f3847ec6f3 | ||
|
e1fe8d1d89 | ||
|
675c937a4a | ||
|
c8f53bdf8e | ||
|
3541d5adde | ||
|
b52da7c9fc | ||
|
de57daa3cd | ||
|
e70e011a9c | ||
|
99febb99f1 | ||
|
874d79be36 | ||
|
b33663c9f8 | ||
|
7e69fa39eb | ||
|
de06490539 | ||
|
40a30c24a0 | ||
|
de04c12d3c | ||
|
38fb53b058 | ||
|
ed617c5943 | ||
|
986337da0c | ||
|
8dd7621f29 | ||
|
88d862303d | ||
|
cc274ffebe | ||
|
28a108f79b | ||
|
b9f75bf7d2 | ||
|
39994d5797 | ||
|
8e28be6558 | ||
|
8a65bef004 | ||
|
b94dc5044b | ||
|
b853c00dd4 | ||
|
7a0bc81f48 | ||
|
10bc326490 | ||
|
1920f8158e | ||
|
0ed2ba0183 | ||
|
95adc0aec1 | ||
|
ebee80d10e | ||
|
63836185d9 | ||
|
d0195e0509 | ||
|
5d9bcd9918 | ||
|
db04c26d24 | ||
|
56b399655e | ||
|
24e15c0568 | ||
|
f0c516e82d | ||
|
f38203ef62 | ||
|
a77c026803 | ||
|
5b6306671c | ||
|
25610222bc | ||
|
c17f941fb9 | ||
|
ae6ab1d203 | ||
|
92accf99b4 | ||
|
b02702fe80 | ||
|
5c549ec6e5 | ||
|
af459a5a28 | ||
|
07770601f6 | ||
|
cc96b86b3a | ||
|
1547f4d8b2 | ||
|
75054fcc70 | ||
|
390b3b173b | ||
|
78daa65d28 | ||
|
3bbdd08d24 | ||
|
cec1f12918 | ||
|
d923ae2107 | ||
|
85931155e6 | ||
|
4fd87aca09 | ||
|
600e0ec7e3 | ||
|
51fbff1a4a | ||
|
03b1389ee5 | ||
|
8f014e9d82 | ||
|
cecc6f7561 | ||
|
62ba81c6a6 | ||
|
c5e3422fcd | ||
|
bd5a46b4ab | ||
|
3a972bbbab | ||
|
7768ea28bd | ||
|
75add44e86 | ||
|
7d94365cbf | ||
|
2d830fb8e7 | ||
|
633bf36fe7 | ||
|
c0a5e23d95 | ||
|
d3798344dd | ||
|
ed37460402 | ||
|
9b6ba65cdb | ||
|
42a9631926 | ||
|
676a8a6421 | ||
|
cdbf022ce0 | ||
|
db79e1271e | ||
|
d2b3efacf9 | ||
|
a2ab94f971 | ||
|
a0d92d764b | ||
|
649b78e3f2 | ||
|
e7df1c3e56 | ||
|
53833ae0c3 | ||
|
66b914774a | ||
|
fc89feec4e | ||
|
d311dbd9d5 | ||
|
f97aa67100 | ||
|
9a8add780c | ||
|
3b48f1d042 | ||
|
332b54e7a5 | ||
|
007b2f0c88 | ||
|
3f083862e7 | ||
|
39619d5277 | ||
|
d4fe01f9b9 | ||
|
6db61b4357 | ||
|
f245cbf7f2 | ||
|
6f2b04669f | ||
|
9a46081d0b | ||
|
3c7e507ca1 | ||
|
7117725e69 | ||
|
ba428c6cfe | ||
|
d76c924ad1 | ||
|
cad7debc5b | ||
|
40725aa2a2 | ||
|
6034891fed | ||
|
9dd9862d33 | ||
|
48c72e319b | ||
|
4b6208fd9c | ||
|
168904a159 | ||
|
28f1498ec3 | ||
|
d3028e10d3 | ||
|
5eb0b77a8a | ||
|
4aace5b95a | ||
|
044dbd4a65 | ||
|
5dbae7c9d7 | ||
|
ec44cb2761 | ||
|
b06cf55c0b | ||
|
b601f6a138 | ||
|
ddebc63488 | ||
|
35440822be | ||
|
f4c6bcfb8e | ||
|
6365c5c9ef | ||
|
dd0334d30d | ||
|
c462a44973 | ||
|
6f88f5db83 | ||
|
93617f62a2 | ||
|
61d5f39408 | ||
|
c1fddaa7dd | ||
|
188aa14d82 | ||
|
5c25dd5b6d | ||
|
7c579cf7b7 | ||
|
c755c823fa | ||
|
116588c237 | ||
|
f02c1e4dc7 | ||
|
c4e8cc1641 | ||
|
93e68ad147 | ||
|
7ba88a83f0 | ||
|
c9293327ce | ||
|
fa1f35a89e | ||
|
845ce7a711 | ||
|
2b40007563 | ||
|
217034c4a7 | ||
|
0b9d4f17ab | ||
|
7fb0ec12dd | ||
|
3581158a7b | ||
|
facfa73214 | ||
|
0ef4a86d42 | ||
|
d4ec4795c3 | ||
|
b13d0aa283 | ||
|
a6965342e7 | ||
|
752dfa5b7f | ||
|
87aa283f22 | ||
|
6598ae080f | ||
|
7c5e8a66e4 | ||
|
c9577bcdc5 | ||
|
8254c2e83c | ||
|
20a9ac841d | ||
|
ae86b75d89 | ||
|
93a0afe612 | ||
|
439027220b | ||
|
4a07272d7a | ||
|
81432b54a3 | ||
|
b84a6e0c02 | ||
|
37dc5a00e8 | ||
|
e6edf85fbe | ||
|
cb533a26f2 | ||
|
a7278f76a8 | ||
|
80bd32382f | ||
|
db21ced104 | ||
|
717c6555cb | ||
|
a412e4af5c | ||
|
3350bf1ac6 | ||
|
4aa3353a1d | ||
|
ff48a58537 | ||
|
08fa511d17 | ||
|
c295115ffc | ||
|
5fb14610ec | ||
|
e87c2350b7 | ||
|
44e691e840 | ||
|
b5a7234cf3 | ||
|
d12509957f | ||
|
f01e7b7e20 | ||
|
6aa156d956 | ||
|
ef5ea93de1 | ||
|
b4913f51f2 | ||
|
dc3e960e79 | ||
|
0fe79b5288 | ||
|
1f76bd1942 | ||
|
3545f80920 | ||
|
0b2d1564ef | ||
|
fdacf824b3 | ||
|
5c01a44644 | ||
|
4eb49d872b | ||
|
34e5f29419 | ||
|
f4910f0a8e | ||
|
c8c14611dc | ||
|
491201991e | ||
|
401f3574fd | ||
|
173a86172c | ||
|
9ecbff024a | ||
|
1b5be34be4 | ||
|
ceb3a997b6 | ||
|
b1ead7fec8 | ||
|
d534dbb006 | ||
|
e56377117b | ||
|
63483dc6c3 | ||
|
66ceafd010 | ||
|
dd793650c3 | ||
|
afd829307d | ||
|
09abdc0f12 | ||
|
ed4d17f578 | ||
|
aeeeb5a37b | ||
|
dcb2e51587 | ||
|
cbc2eaf908 | ||
|
8808031e7c | ||
|
23ac7213d3 | ||
|
add7b44d0b | ||
|
1e4b7599a7 | ||
|
54443b038a | ||
|
c3f03e3f95 | ||
|
18135624f6 | ||
|
11238d6b71 | ||
|
848f94b1e0 | ||
|
d47cfe9504 | ||
|
70dccff293 | ||
|
e40873710b | ||
|
b140ef3b7a | ||
|
3b7b74aa67 | ||
|
de8e5b2d69 | ||
|
afea33b0e3 | ||
|
b44fbc1e4f | ||
|
c4dee3dd8d | ||
|
091e024032 | ||
|
0e030f7f48 | ||
|
3341c9e3bf | ||
|
91ddc00f7e | ||
|
3049ba0b24 | ||
|
55a161fafe | ||
|
349af24c81 | ||
|
788f1c4b3e | ||
|
889af461c6 | ||
|
df86e59089 | ||
|
c6bf69cce4 | ||
|
e492012004 | ||
|
94c46f9881 | ||
|
1eaa9d32ea | ||
|
0e2e8d2e2a | ||
|
cfb39c6364 | ||
|
173499f496 | ||
|
1081049074 | ||
|
961dc85514 | ||
|
6434acd492 | ||
|
ccb27c89d8 | ||
|
8053256203 | ||
|
4abd7301fd | ||
|
d6fe5ab417 | ||
|
a739fbdf1d | ||
|
c90a1ab6dc | ||
|
05ef68e079 | ||
|
dee4a7f3c7 | ||
|
75753df0d8 | ||
|
30c5d78647 | ||
|
6bb4db3842 | ||
|
cc0907fcd7 | ||
|
20b018bcc7 | ||
|
aafe2fa8d0 | ||
|
ee7bd73b3f | ||
|
b67f3bd629 | ||
|
38b81e79f8 | ||
|
b73c549131 | ||
|
d21d9f0141 | ||
|
7bb85032a1 | ||
|
d90446ad28 | ||
|
8b5e2f5528 | ||
|
1bcc3ab7f1 | ||
|
3359c3cd45 | ||
|
af812f3c90 | ||
|
1e6201093b | ||
|
959ea69427 | ||
|
cea744a914 | ||
|
e8baf48764 | ||
|
41242a2ae1 | ||
|
f1dee488a7 | ||
|
3b4ff1818e | ||
|
497145b1b5 | ||
|
10eb41d319 | ||
|
4daf2e4a3d | ||
|
f3266a5111 | ||
|
27cac4e8b8 | ||
|
60b9a5b9da | ||
|
eaaa62a7f3 | ||
|
6ce732ec3d | ||
|
fb0cc61e09 | ||
|
be29b5daf8 | ||
|
f010adabd0 | ||
|
c93b263b1f | ||
|
15f34d6b54 | ||
|
d0eeb55999 | ||
|
08c0d39b23 | ||
|
d0ecde3277 | ||
|
45ec57afd7 | ||
|
b0cd053083 | ||
|
698a11be58 | ||
|
efb08fb1e1 | ||
|
f89bc8422e | ||
|
4bf4889a08 | ||
|
01ab8ba38e | ||
|
ae6d15e812 | ||
|
1abfbe1d34 | ||
|
d3095297c2 | ||
|
79d40d5644 | ||
|
008e305a84 | ||
|
0379611edd | ||
|
96d883c1c7 | ||
|
be0f262e37 | ||
|
1676adf071 | ||
|
a5d5630067 | ||
|
275956caba | ||
|
bf8ed87fc9 | ||
|
fb3afac097 | ||
|
eda8b037a9 | ||
|
8ef14f7a54 | ||
|
9974e35656 | ||
|
15bc5431b6 | ||
|
f767531d89 | ||
|
7285ada9dd | ||
|
b9b9773df9 | ||
|
c29a83b259 | ||
|
fa45e66da6 | ||
|
7cbcdddac9 | ||
|
a2f17900da | ||
|
fb7e97b8ad | ||
|
0c92a8a8e9 | ||
|
dcc59380e5 | ||
|
f9bf25f96d | ||
|
cbcbea8b08 | ||
|
62eb4f20da | ||
|
43d5311e5e | ||
|
a6f08a09d5 | ||
|
d93f5d7785 | ||
|
c7170e6dc2 | ||
|
bcf3ca7339 | ||
|
0388f5787a | ||
|
6514df9d96 | ||
|
8f5b9869dc | ||
|
580c5fe23f | ||
|
b0af5b26ba | ||
|
9e898932f6 | ||
|
1f873b93f6 | ||
|
f414707f11 | ||
|
505825056c | ||
|
38f7716738 | ||
|
a69d08b554 | ||
|
3ccdb64833 | ||
|
78d8bff599 | ||
|
a756fed70b | ||
|
3e2a1e3548 | ||
|
96b2f2b3a4 | ||
|
d81d7d4f68 | ||
|
20244c4fb5 | ||
|
b50d31ffe2 | ||
|
d709a44960 | ||
|
4e4d07ced6 | ||
|
d775bc9d7e | ||
|
85528761eb | ||
|
ad3eac9ddb | ||
|
613f9fccd2 | ||
|
3d1741c904 | ||
|
26be14ba67 | ||
|
daa0755920 | ||
|
305d60e09b | ||
|
d0029efd02 | ||
|
fb4d42bf5b | ||
|
20eec53b14 | ||
|
8343db44db | ||
|
649652e373 | ||
|
e37ed7c32d | ||
|
f9a525068b | ||
|
aa11e6d62e | ||
|
6802d152da | ||
|
3b40f393d8 | ||
|
020443ae8a | ||
|
1d0baccffc | ||
|
5426f0f329 | ||
|
790249dd1a | ||
|
446a201d25 | ||
|
b6538d5e18 | ||
|
edd6043059 | ||
|
fe4ffeb7f1 | ||
|
27b3875bfb | ||
|
e2dbe8a0a2 | ||
|
29fc7910b7 | ||
|
22e2fdc707 | ||
|
584786eb9f | ||
|
d803d9eaf9 | ||
|
bad6575d83 | ||
|
fbcb7ae836 | ||
|
2c1a1b10c8 | ||
|
155fb16a8a | ||
|
b1ab2ce96a | ||
|
f299ba6218 | ||
|
d167ad1923 | ||
|
93626e8154 | ||
|
d5040c091a | ||
|
868daef0f0 | ||
|
dc8e85e7f2 | ||
|
22d32d7ca3 | ||
|
2d500f8074 | ||
|
452cdc17c6 | ||
|
bcbfee0321 | ||
|
dab2e7ede3 | ||
|
d91acb8352 | ||
|
373dd8058e | ||
|
de6310e52a | ||
|
8c297a4a4c | ||
|
4eb5c817bc | ||
|
07e4b26b9e | ||
|
b63aa62985 | ||
|
ca701c0580 | ||
|
e37043a6a8 | ||
|
7c26975d14 | ||
|
47f8a43637 | ||
|
1238c0cefe | ||
|
4c35fda045 | ||
|
780124c2f5 | ||
|
38e0af41ce | ||
|
601e99eec0 | ||
|
745a2adee7 | ||
|
9e83234df1 | ||
|
e45eeadf7d | ||
|
9a778cea6b | ||
|
20823bfc87 | ||
|
be3d703692 | ||
|
e2df5739f0 | ||
|
7bb11d6436 | ||
|
80b84212cc | ||
|
4a1bee769b | ||
|
de99a7aeaf | ||
|
a0a02701b0 | ||
|
1314ae1555 | ||
|
67cf0e745c | ||
|
538a2acbf5 | ||
|
8d0e453666 | ||
|
ace04f0b30 | ||
|
f8e25d6c4a | ||
|
b2bc43da4f | ||
|
cb12e540d2 | ||
|
39955af2fa | ||
|
86e1f0615d | ||
|
dc81ab6dee | ||
|
f8cf6a65ae | ||
|
ba909c696d | ||
|
df0515cebf | ||
|
46c0e14d67 | ||
|
97d7733464 | ||
|
afda84ef09 | ||
|
61d6e74102 | ||
|
8d74258ce2 | ||
|
5dfba0b834 | ||
|
056370ec08 | ||
|
4e2c254558 | ||
|
38c2bdb447 | ||
|
0cee4717e2 | ||
|
221b04c466 | ||
|
237e9b7191 | ||
|
9457e44a88 | ||
|
80a9d40a44 | ||
|
0715325a63 | ||
|
0a026fef0f | ||
|
5fbf650d2d | ||
|
dabdde0c3f | ||
|
b6ca92a7ef | ||
|
62f7339170 | ||
|
eaec682ea7 | ||
|
12110a4442 | ||
|
df597f53b2 | ||
|
d7d40254d4 | ||
|
e7c4a2cce6 | ||
|
0eb1c0cea6 | ||
|
07d35a8513 | ||
|
16c887814e | ||
|
22a50b72fd | ||
|
a79e1b6ca1 | ||
|
995296ef53 | ||
|
f4d5996a88 | ||
|
ab732b5435 | ||
|
241ff5cb6e | ||
|
ec2169e079 | ||
|
c75662e720 | ||
|
d567fd4842 | ||
|
0776dfc80e | ||
|
79a662fb93 | ||
|
8b009b7ee9 | ||
|
c4face30cc | ||
|
d4dbb5cb51 | ||
|
1e27187652 | ||
|
3ff278291f | ||
|
4e8bf216df | ||
|
e7b9100f1c | ||
|
08aa9790f3 | ||
|
5f13fd2dca | ||
|
f4c6f42c38 | ||
|
6a10d08189 | ||
|
f646360af6 | ||
|
95f265ebbf | ||
|
39d0142993 | ||
|
da16172244 | ||
|
71aded0fae | ||
|
e173b3ea41 | ||
|
6c4f9466b9 | ||
|
664196c5ef | ||
|
c8d5044e7a | ||
|
d8c31fe560 | ||
|
516db855f5 | ||
|
9cdcf08ab1 | ||
|
326fa73b22 | ||
|
79cacefd07 | ||
|
07e28bfee6 | ||
|
980b017fbe | ||
|
4567fd1eb0 | ||
|
8c150c23f3 | ||
|
b4fd570269 | ||
|
681a845ef3 | ||
|
6e051d73c1 | ||
|
e381e1a313 | ||
|
2d03ff63cf | ||
|
309de631ed | ||
|
ad240cf52f | ||
|
99e3a47dde | ||
|
3fa810b7b8 | ||
|
bad7316b80 | ||
|
4757c36233 | ||
|
9ca6180207 | ||
|
30179ad977 | ||
|
cd129fb055 | ||
|
880a977dd6 | ||
|
795e33881c | ||
|
837e275bfd | ||
|
7be6031e19 | ||
|
3f9181905a | ||
|
5efc43260e | ||
|
e01794a07f | ||
|
ff9c6bac0a | ||
|
713111254b | ||
|
5b1462a3e8 | ||
|
2bf18d8bda | ||
|
7f2e643e62 | ||
|
ef172592b8 |
@@ -5,12 +5,18 @@
|
||||
"max-len": ["error", 250],
|
||||
"curly": "error",
|
||||
"camelcase": ["error", {"properties": "never"}],
|
||||
"no-trailing-spaces": ["error"],
|
||||
"no-trailing-spaces": ["error", {"ignoreComments": false }],
|
||||
"no-irregular-whitespace": ["error"]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
}
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
.gitignore
vendored
@@ -19,6 +19,9 @@ jspm_modules
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
|
||||
# IDE Code ignoramuses.
|
||||
.idea/
|
||||
|
||||
# Various Windows ignoramuses.
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
@@ -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"
|
||||
|
298
CHANGELOG.md
@@ -1,7 +1,271 @@
|
||||
# MagicMirror² Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² core.
|
||||
|
||||
## [2.8.0] - Unreleased
|
||||
|
||||
*This release is scheduled to be released on 2019-04-01.*
|
||||
|
||||
### Added
|
||||
|
||||
### Updated
|
||||
|
||||
### Fixed
|
||||
|
||||
## [2.7.0] - 2019-04-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
- Italian translation for "Feels"
|
||||
- Basic Klingon (tlhIngan Hol) translations
|
||||
- Disabled the screensaver on raspbian with installation script
|
||||
- Added option to truncate the number of vertical lines a calendar item can span if `wrapEvents` is enabled.
|
||||
- Danish translation for "Feels" and "Weeks"
|
||||
- Added option to split multiple day events in calendar to separate numbered events
|
||||
- Slovakian translation
|
||||
- Alerts now can contain Font Awesome icons
|
||||
- Notifications display time can be set in request
|
||||
- Newsfeed: added support for `ARTICLE_INFO_REQUEST` notification
|
||||
- Add `name` config option for calendars to be sent along with event broadcasts
|
||||
|
||||
### Updated
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
|
||||
- Updated modernizr code in alert module, fixed a small typo there too
|
||||
- More verbose error message on console if the config is malformed
|
||||
- Updated installer script to install Node.js version 10.x
|
||||
|
||||
### Fixed
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
- Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611)
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Installation script problems with raspbian
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||
- Notification: fixed background color (was white text on white background)
|
||||
- Use getHeader instead of data.header when creating the DOM so overwriting the function also propagates into it
|
||||
- Fix documentation of `useKMPHwind` option in currentweather
|
||||
|
||||
### New weather module
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Dimmed loading indicator for weather forecast.
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Added humidity support to nunjuck unit filter.
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MichMich/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MichMich/MagicMirror/issues/1504), [#1513](https://github.com/MichMich/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||
- Added weather.gov as a new weather provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues updating, make sure you are running the latest version of Node.
|
||||
|
||||
### ✨ Experimental ✨
|
||||
- New default [module weather](modules/default/weather). This module will eventually replace the current `currentweather` and `weatherforecast` modules. The new module is still pretty experimental, but it's included so you can give it a try and help us improve this module. Please give us you feedback using [this forum post](https://forum.magicmirror.builders/topic/9335/default-weather-module-refactoring).
|
||||
|
||||
A huge, huge, huge thanks to user @fewieden for all his hard work on the new `weather` module!
|
||||
|
||||
### Added
|
||||
- Possibility to add classes to the cell of symbol, title and time of the events of calendar.
|
||||
- Font-awesome 5, still has 4 for backwards compatibility.
|
||||
- Missing `showEnd` in calendar documentation
|
||||
- Screenshot for the new feed module
|
||||
- Screenshot for the compliments module
|
||||
- Screenshot for the clock module
|
||||
- Screenshot for the current weather
|
||||
- Screenshot for the weather forecast module
|
||||
- Portuguese translation for "Feels"
|
||||
- Croatian translation
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MichMich/MagicMirror/issues/1464)
|
||||
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||
|
||||
### Fixed
|
||||
- Allow to parse recurring calendar events where the start date is before 1900
|
||||
- Fixed Polish translation for Single Update Info
|
||||
- Ignore entries with unparseable details in the calendar module
|
||||
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||
|
||||
### Updated
|
||||
- The default calendar setting `showEnd` is changed to `false`.
|
||||
|
||||
### Changed
|
||||
- The Weather Forecast module by default displays the ° symbol after every numeric value to be consistent with the Current Weather module.
|
||||
|
||||
|
||||
## [2.5.0] - 2018-10-01
|
||||
|
||||
### Added
|
||||
- Romanian translation for "Feels"
|
||||
- Support multi-line compliments
|
||||
- Simplified Chinese translation for "Feels"
|
||||
- Polish translate for "Feels"
|
||||
- French translate for "Feels"
|
||||
- Translations for newsfeed module
|
||||
- Support for toggling news article in fullscreen
|
||||
- Hungarian translation for "Feels" and "Week"
|
||||
- Spanish translation for "Feels"
|
||||
- Add classes instead of inline style to the message from the module Alert
|
||||
- Support for events having a duration instead of an end
|
||||
- Support for showing end of events through config parameters showEnd and dateEndFormat
|
||||
|
||||
### Fixed
|
||||
- Fixed gzip encoded calendar loading issue #1400.
|
||||
- Mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix weather city ID link in sample config
|
||||
- Fixed issue with clientonly not updating with IP address and port provided on command line.
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated Simplified Chinese translation
|
||||
- Swedish translations
|
||||
- Hungarian translations for the updatenotification module
|
||||
- Updated Norsk bokmål translation
|
||||
- Updated Norsk nynorsk translation
|
||||
- Consider multi days event as full day events
|
||||
|
||||
## [2.4.1] - 2018-07-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix weather parsing issue #1332.
|
||||
|
||||
## [2.4.0] - 2018-07-01
|
||||
|
||||
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Added
|
||||
|
||||
- Enabled translation of feelsLike for module currentweather
|
||||
- Added support for on-going calendar events
|
||||
- Added scroll up in fullscreen newsfeed article view
|
||||
- Changed fullscreen newsfeed width from 100% to 100vw (better results)
|
||||
- Added option to calendar module that colors only the symbol instead of the whole line
|
||||
- Added option for new display format in the calendar module with date headers with times/events below.
|
||||
- Ability to fetch compliments from a remote server
|
||||
- Add regex filtering to calendar module
|
||||
- Customize classes for table
|
||||
- Added option to newsfeed module to only log error parsing a news article if enabled
|
||||
- Add update translations for Português Brasileiro
|
||||
|
||||
### Changed
|
||||
- Upgrade to Electron 2.0.0.
|
||||
- Remove yarn-or-npm which breaks production builds.
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
|
||||
|
||||
### Fixed
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
|
||||
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
|
||||
- Fixed browser-side code to work on the Midori browser.
|
||||
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
- Fixed weatherforecast to use dt_txt field instead of dt to handle timezones better
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MichMich/MagicMirror/issues/1282)
|
||||
- `clientonly/*.js` is now linted, and one linting error is fixed
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with momentjs.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
- Updated Italian translation
|
||||
- Updated German translation
|
||||
- Updated Dutch translation
|
||||
|
||||
## [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
|
||||
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Changed
|
||||
- Calender week is now handled with a variable translation in order to move number language specific.
|
||||
- Reverted the Electron dependency back to 1.4.15 since newer version don't seem to work on the Raspberry Pi very well.
|
||||
|
||||
### Added
|
||||
- Add option to use [Nunjucks](https://mozilla.github.io/nunjucks/) templates in modules. (See `helloworld` module as an example.)
|
||||
- Add Bulgarian translations for MagicMirror² and Alert module.
|
||||
- Add graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting.
|
||||
- Link update subtext to Github diff of current version versus tracking branch.
|
||||
- Add Catalan translation.
|
||||
- Add ability to filter out newsfeed items based on prohibited words found in title (resolves #1071)
|
||||
- Add options to truncate description support of a feed in newsfeed module
|
||||
- Add reloadInterval option for particular feed in newsfeed module
|
||||
- Add no-cache entries of HTTP headers in newsfeed module (fetcher)
|
||||
- Add Czech translation.
|
||||
- Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather.
|
||||
|
||||
### Fixed
|
||||
- Fixed issue with calendar module showing more than `maximumEntries` allows
|
||||
- WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP
|
||||
- Correcting translation for Indonesian language
|
||||
- Fix issue where calendar icons wouldn't align correctly
|
||||
|
||||
## [2.1.3] - 2017-10-01
|
||||
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
@@ -13,7 +277,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Add `clientonly` script to start only the electron client for a remote server.
|
||||
- Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module.
|
||||
- Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git.
|
||||
- Add unit test the capitalizeFirstLetter function of newfeed module.
|
||||
- Add unit test the capitalizeFirstLetter function of newsfeed module.
|
||||
- Add new unit tests for function `shorten` in calendar module.
|
||||
- Add new unit tests for function `getLocaleSpecification` in calendar module.
|
||||
- Add unit test for js/class.js.
|
||||
@@ -29,12 +293,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Changed 'default.js' - listen on all attached interfaces by default.
|
||||
- Add execution of `npm list` after the test are ran in Travis CI.
|
||||
- Change hooks for the vendors e2e tests.
|
||||
- Add log when clientonly failed on starting.
|
||||
- Add log when clientonly failed on starting.
|
||||
- Add warning color when are using full ip whitelist.
|
||||
- Set version of the `express-ipfilter` on 0.3.1.
|
||||
|
||||
### Fixed
|
||||
- Fixed issue with incorrect allignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||
@@ -58,7 +322,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Add unit test calendar_modules function capFirst.
|
||||
- Add test for check if exists the directories present in defaults modules.
|
||||
- Add support for showing wind direction as an arrow instead of abbreviation in currentWeather module.
|
||||
- Add support for writing translation fucntions to support flexible word order
|
||||
- Add support for writing translation functions to support flexible word order
|
||||
- Add test for check if exits the directories present in defaults modules.
|
||||
- Add calendar option to set a separate date format for full day events.
|
||||
- Add ability for `currentweather` module to display indoor temperature via INDOOR_TEMPERATURE notification
|
||||
@@ -77,7 +341,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Fix double message about port when server is starting
|
||||
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
|
||||
- Removed unused import from js/electron.js
|
||||
- Made calendar.js respect config.timeFormat irrespecive of locale setting.
|
||||
- Made calendar.js respect config.timeFormat irrespective of locale setting.
|
||||
- Fixed alignment of analog clock when a large calendar is displayed in the same side bar.
|
||||
|
||||
## [2.1.1] - 2017-04-01
|
||||
@@ -95,7 +359,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `DAYAFTERTOMORROW`, `UPDATE_NOTIFICATION` and `UPDATE_NOTIFICATION_MODULE` to Finnish translations.
|
||||
- Run `npm test` on Travis automatically.
|
||||
- Show the splash screen image even when is reboot or halted.
|
||||
- Added some missing translaton strings in the sv.json file.
|
||||
- Added some missing translation strings in the sv.json file.
|
||||
- Run task jsonlint to check translation files.
|
||||
- Restructured Test Suite.
|
||||
|
||||
@@ -112,12 +376,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Option to use RegExp in Calendar's titleReplace.
|
||||
- Hungarian Translation.
|
||||
- Icelandic Translation.
|
||||
- Add use a script to prevent when is run by SSH session set DISPLAY enviroment.
|
||||
- Enable ability to set configuration file by the enviroment variable called MM_CONFIG_FILE.
|
||||
- Add use a script to prevent when is run by SSH session set DISPLAY environment.
|
||||
- Enable ability to set configuration file by the environment variable called MM_CONFIG_FILE.
|
||||
- Option to give each calendar a different color.
|
||||
- Option for colored min-temp and max-temp.
|
||||
- Add test e2e helloworld.
|
||||
- Add test e2e enviroment.
|
||||
- Add test e2e environment.
|
||||
- Add `chai-as-promised` npm module to devDependencies.
|
||||
- Basic set of tests for clock module.
|
||||
- Run e2e test in Travis.
|
||||
@@ -135,10 +399,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added tests for Translations, dev argument, version, dev console.
|
||||
- Added test anytime feature compliments module.
|
||||
- Added test ipwhitelist configuration directive.
|
||||
- Added test for calendar module: default, basic-auth, backward compability, fail-basic-auth.
|
||||
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||
- Added test for MM_PORT enviroment variable.
|
||||
- Added test for MM_PORT environment variable.
|
||||
- Added a configurable Week section to the clock module.
|
||||
|
||||
### Fixed
|
||||
@@ -150,7 +414,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Module currentWeather: check if temperature received from api is defined.
|
||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||
- Fix newsfeed module bug (removeStartTags)
|
||||
- Fix when is set MM_PORT enviroment variable.
|
||||
- Fix when is set MM_PORT environment variable.
|
||||
- Fixed missing animation on `this.show(speed)` when module is alone in a region.
|
||||
|
||||
## [2.1.0] - 2016-12-31
|
||||
@@ -172,8 +436,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
- Add module `updatenotification` to get an update whenever a new version is availabe. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the abilty to set timezone on the date display in the Clock Module
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the ability to set timezone on the date display in the Clock Module
|
||||
- Ability to set date format in calendar module
|
||||
- Possibility to use currentweather for the compliments
|
||||
- Added option `disabled` for modules.
|
||||
@@ -212,7 +476,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
|
||||
- Added CII Badge (we are compliant with the CII Best Practices)
|
||||
- Add support for doing http basic auth when loading calendars
|
||||
- Add the abilty to turn off and on the date display in the Clock Module
|
||||
- Add the ability to turn off and on the date display in the Clock Module
|
||||
|
||||
### Fixed
|
||||
- Fix typo in installer.
|
||||
@@ -235,8 +499,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Fixed
|
||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||
- Append endpoints of weather modules with `/` to retreive the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Corrected grammer in `module.js` from 'suspend' to 'suspended'.
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||
- Fixed openweathermap.org URL in config sample.
|
||||
- Prevent currentweather module from crashing when received data object is incorrect.
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||
|
@@ -11,6 +11,7 @@ module.exports = function(grunt) {
|
||||
"modules/default/*.js",
|
||||
"modules/default/*/*.js",
|
||||
"serveronly/*.js",
|
||||
"clientonly/*.js",
|
||||
"*.js",
|
||||
"tests/**/*.js",
|
||||
"!modules/default/alert/notificationFx.js",
|
||||
|
200
README.md
@@ -7,7 +7,6 @@
|
||||
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://travis-ci.org/MichMich/MagicMirror"><img src="https://travis-ci.org/MichMich/MagicMirror.svg" alt="Travis"></a>
|
||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
||||
<a href="http://slack.magicmirror.builders"><img src="http://slack.magicmirror.builders:3000/badge.svg" alt="Slack Status"></a>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](http://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -16,120 +15,135 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Table Of Contents](#table-of-contents)
|
||||
- [Installation](#installation)
|
||||
- [Raspberry Pi](#raspberry-pi)
|
||||
- [Automatic Installation (Raspberry Pi only!)](#automatic-installation-raspberry-pi-only)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [Server Only](#server-only)
|
||||
- [Client Only](#client-only)
|
||||
- [Docker](#docker)
|
||||
- [Configuration](#configuration)
|
||||
- [Raspberry Specific](#raspberry-specific)
|
||||
- [General](#general)
|
||||
- [Modules](#modules)
|
||||
- [Known Issues](#known-issues)
|
||||
- [Updating](#updating)
|
||||
- [Community](#community)
|
||||
- [Contributing Guidelines](#contributing-guidelines)
|
||||
- [Enjoying MagicMirror? Consider a donation!](#enjoying-magicmirror-consider-a-donation)
|
||||
- [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 latest 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:
|
||||
- `curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -`
|
||||
- `sudo apt install -y nodejs`
|
||||
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 `/home/pi/MagicMirror/config/config.js.sample` to `/home/pi/MagicMirror/config/config.js`. \
|
||||
**Note:** If you used the installer script. This step is already done for you.
|
||||
|
||||
2. Modify your required settings. \
|
||||
Note: You can check your configuration running `npm run config:check` in `/home/pi/MagicMirror`.
|
||||
|
||||
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 `::` is IPv6 is available or `0.0.0.0` IPv4 run on. Example config: `192.168.10.100`. |
|
||||
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`). 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`. |
|
||||
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
|
||||
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
|
||||
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk = true`, `autoHideMenuBar = false` and `fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
||||
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk: true`, `autoHideMenuBar: false` and `fullscreen: false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
||||
| `customCss` | The path of the `custom.css` stylesheet. The default is `css/custom.css`. |
|
||||
|
||||
Module configuration:
|
||||
@@ -137,7 +151,7 @@ Module configuration:
|
||||
| **Option** | **Description** |
|
||||
| --- | --- |
|
||||
| `module` | The name of the module. This can also contain the subfolder. Valid examples include `clock`, `default/calendar` and `custommodules/mymodule`. |
|
||||
| `position` | The location of the module in which the module will be loaded. Possible values are `top_ bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. |
|
||||
| `position` | The location of the module in which the module will be loaded. Possible values are `top_bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. |
|
||||
| `classes` | Additional classes which are passed to the module. The field is optional. |
|
||||
| `header` | To display a header text above the module, add the header property. This field is optional. |
|
||||
| `disabled` | Set disabled to `true` to skip creating the module. This field is optional. |
|
||||
@@ -156,12 +170,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 +202,32 @@ Please keep the following in mind:
|
||||
|
||||
Thanks for your help in making MagicMirror² better!
|
||||
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we recieve enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
## 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>
|
||||
|
@@ -62,13 +62,13 @@
|
||||
// Only start the client if a non-local server was provided
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
|
||||
getServerConfig(`http://${config.address}:${config.port}/config/`)
|
||||
.then(function (config) {
|
||||
.then(function (configReturn) {
|
||||
// Pass along the server config via an environment variable
|
||||
var env = Object.create(process.env);
|
||||
var options = { env: env };
|
||||
config.address = config.address;
|
||||
config.port = config.port;
|
||||
env.config = JSON.stringify(config);
|
||||
configReturn.address = config.address;
|
||||
configReturn.port = config.port;
|
||||
env.config = JSON.stringify(configReturn);
|
||||
|
||||
// Spawn electron application
|
||||
const electron = require("electron");
|
||||
@@ -88,7 +88,7 @@
|
||||
process.stdout.write(`Client: ${err}`);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
child.on("close", (code) => {
|
||||
if (code != 0) {
|
||||
console.log(`There something wrong. The clientonly is not running code ${code}`);
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ var config = {
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar-check-o ",
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
||||
}
|
||||
]
|
||||
@@ -59,7 +59,7 @@ var config = {
|
||||
position: "top_right",
|
||||
config: {
|
||||
location: "New York",
|
||||
locationID: "", //ID from http://www.openweathermap.org/help/city_list.txt
|
||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
@@ -69,7 +69,7 @@ var config = {
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://www.openweathermap.org/help/city_list.txt
|
||||
locationID: "5128581", //ID from https://openweathermap.org/city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
|
19
css/main.css
@@ -95,7 +95,7 @@ body {
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-family: "Roboto Condensed";
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #666;
|
||||
line-height: 15px;
|
||||
@@ -128,6 +128,10 @@ sup {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Region Definitions.
|
||||
*/
|
||||
@@ -151,6 +155,7 @@ sup {
|
||||
|
||||
.region.right {
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.region.top {
|
||||
@@ -161,6 +166,10 @@ sup {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.region.bottom .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.region.top .container:empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -185,10 +194,6 @@ sup {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.region.bottom .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.region.bottom .container:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -231,10 +236,6 @@ sup {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.region.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.region table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
|
17
dangerfile.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { danger, fail, warn } from "danger"
|
||||
|
||||
// Check if the CHANGELOG.md file has been edited
|
||||
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.")
|
||||
}
|
||||
|
||||
// Check if the PR request is send to the master branch.
|
||||
// This should only be done by MichMich.
|
||||
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||
// Check if the PR body or title includes the text: #accepted.
|
||||
// If not, the PR will fail.
|
||||
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.")
|
||||
}
|
||||
}
|
12
fonts/package-lock.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"roboto-fontface": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.8.0.tgz",
|
||||
"integrity": "sha512-ZYzRkETgBrdEGzL5JSKimvjI2CX7ioyZCkX2BpcfyjqI+079W0wHAyj5W4rIZMcDSOHgLZtgz1IdDi/vU77KEQ=="
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,6 +10,6 @@
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.8.0"
|
||||
"roboto-fontface": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
7
fonts/yarn.lock
Normal 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"
|
@@ -38,6 +38,7 @@
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
|
@@ -42,12 +42,12 @@ 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"
|
||||
NODE_INSTALL=false
|
||||
if command_exists node; then
|
||||
if command_exists node && command_exists npm; then
|
||||
echo -e "\e[0mNode currently installed. Checking version number.";
|
||||
NODE_CURRENT=$(node -v)
|
||||
echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\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="10.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,12 +149,22 @@ 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"
|
||||
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json
|
||||
pm2 save
|
||||
if [[ "$(ps --no-headers -o comm 1)" =~ systemd ]]; then #Checking for systemd
|
||||
sudo pm2 startup systemd -u pi --hp /home/pi
|
||||
else
|
||||
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"
|
||||
fi
|
||||
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json
|
||||
pm2 save
|
||||
fi
|
||||
# Disable Screensaver
|
||||
read -p "Do you want to disable the screen saver? (y/N)?" choice
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
sudo su -c "echo -e '@xset s noblank\n@xset s off\n@xset -dpms' >> /etc/xdg/lxsession/LXDE-pi/autostart"
|
||||
export DISPLAY=:0; xset s noblank;xset s off;xset -dpms
|
||||
fi
|
||||
|
||||
echo " "
|
||||
|
29
js/app.js
@@ -70,7 +70,7 @@ var App = function() {
|
||||
if (e.code == "ENOENT") {
|
||||
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Please correct syntax errors. Starting with default configuration."));
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
} else {
|
||||
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
}
|
||||
@@ -236,6 +236,33 @@ var App = function() {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* stop()
|
||||
* This methods stops the core app.
|
||||
* This calls each node_helper's STOP() function, if it exists.
|
||||
* Added to fix #1056
|
||||
*/
|
||||
this.stop = function() {
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Listen for SIGINT signal and call stop() function.
|
||||
*
|
||||
* Added to fix #1056
|
||||
* Note: this is only used if running `server-only`. Otherwise
|
||||
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
||||
*/
|
||||
process.on("SIGINT", () => {
|
||||
console.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = new App();
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -96,6 +96,20 @@ app.on("activate", function() {
|
||||
}
|
||||
});
|
||||
|
||||
/* This method will be called when SIGINT is received and will call
|
||||
* each node_helper's stop function if it exists. Added to fix #1056
|
||||
*
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||
*/
|
||||
app.on("before-quit", (event) => {
|
||||
console.log("Shutting down server...");
|
||||
event.preventDefault();
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force-quit after 3 seconds.
|
||||
core.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
|
||||
|
130
js/main.js
@@ -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(function(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.getHeader() !== "undefined" && module.getHeader() !== "") {
|
||||
var moduleHeader = document.createElement("header");
|
||||
moduleHeader.innerHTML = module.getHeader();
|
||||
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(function() {
|
||||
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
||||
}).catch(Log.error);
|
||||
});
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
sendNotification("DOM_OBJECTS_CREATED");
|
||||
Promise.all(domCreationPromises).then(function() {
|
||||
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(function(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(function(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(function(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,22 +157,26 @@ 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?
|
||||
*/
|
||||
var moduleNeedsUpdate = function(module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
|
||||
@@ -152,6 +198,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) {
|
||||
@@ -202,6 +249,9 @@ var MM = (function() {
|
||||
|
||||
if (typeof callback === "function") { callback(); }
|
||||
}, speed);
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") { callback(); }
|
||||
}
|
||||
};
|
||||
|
||||
|
105
js/module.js
@@ -27,6 +27,11 @@ var Module = Class.extend({
|
||||
// visibility when hiding and showing module.
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjuck Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/* init()
|
||||
* Is called when the module is instantiated.
|
||||
*/
|
||||
@@ -70,25 +75,37 @@ var Module = Class.extend({
|
||||
|
||||
/* getDom()
|
||||
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
* This method needs to be subclassed if the module wants to display info on the mirror.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplete method could be subclassed.
|
||||
*
|
||||
* return domobject - The dom to display.
|
||||
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var nameWrapper = document.createElement("div");
|
||||
var name = document.createTextNode(this.name);
|
||||
nameWrapper.appendChild(name);
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var div = document.createElement("div");
|
||||
var template = self.getTemplate();
|
||||
var templateData = self.getTemplateData();
|
||||
|
||||
var identifierWrapper = document.createElement("div");
|
||||
var identifier = document.createTextNode(this.identifier);
|
||||
identifierWrapper.appendChild(identifier);
|
||||
identifierWrapper.className = "small dimmed";
|
||||
// Check to see if we need to render a template string or a file.
|
||||
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||
// the template is a filename
|
||||
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
Log.error(err)
|
||||
}
|
||||
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(nameWrapper);
|
||||
div.appendChild(identifierWrapper);
|
||||
div.innerHTML = res;
|
||||
|
||||
return div;
|
||||
resolve(div);
|
||||
});
|
||||
} else {
|
||||
// the template is a template string.
|
||||
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
|
||||
|
||||
resolve(div);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* getHeader()
|
||||
@@ -102,6 +119,28 @@ var Module = Class.extend({
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
/* getTemplate()
|
||||
* This method returns the template for the module which is used by the default getDom implementation.
|
||||
* This method needs to be subclassed if the module wants to use a tempate.
|
||||
* It can either return a template sting, or a template filename.
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
*
|
||||
* return string - The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return "<div class=\"normal\">" + this.name + "</div><div class=\"small dimmed\">" + this.identifier + "</div>";
|
||||
},
|
||||
|
||||
/* getTemplateData()
|
||||
* This method returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* return Object
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
return {}
|
||||
},
|
||||
|
||||
/* notificationReceived(notification, payload, sender)
|
||||
* This method is called when a notification arrives.
|
||||
* This method is called by the Magic Mirror core.
|
||||
@@ -118,6 +157,30 @@ var Module = Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** nunjucksEnvironment()
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
|
||||
* @returns Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function() {
|
||||
if (this._nunjucksEnvironment != null) {
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), {async: true}), {
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true
|
||||
});
|
||||
this._nunjucksEnvironment.addFilter("translate", function(str) {
|
||||
return self.translate(str)
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
* This method is called when a socket notification arrives.
|
||||
*
|
||||
@@ -149,7 +212,7 @@ var Module = Class.extend({
|
||||
/* setData(data)
|
||||
* Set the module data.
|
||||
*
|
||||
* argument data obejct - Module data.
|
||||
* argument data object - Module data.
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -163,7 +226,7 @@ var Module = Class.extend({
|
||||
/* setConfig(config)
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* argument config obejct - Module config.
|
||||
* argument config object - Module config.
|
||||
*/
|
||||
setConfig: function (config) {
|
||||
this.config = Object.assign({}, this.defaults, config);
|
||||
@@ -276,8 +339,8 @@ var Module = Class.extend({
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* argument key string - The key of the string to translate
|
||||
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||
* argument defaultValue string - The default value with variables. (Optional)
|
||||
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||
* argument defaultValue string - The default value with variables. (Optional)
|
||||
*/
|
||||
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||
if(typeof defaultValueOrVariables === "object") {
|
||||
@@ -414,11 +477,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
|
||||
}
|
||||
}
|
||||
|
@@ -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
@@ -0,0 +1,31 @@
|
||||
type ModuleProperties = {
|
||||
defaults?: object,
|
||||
start?(): void,
|
||||
getHeader?(): string,
|
||||
getTemplate?(): string,
|
||||
getTemplateData?(): object,
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void,
|
||||
socketNotificationReceived?(notification: string, payload: any): void,
|
||||
suspend?(): void,
|
||||
resume?(): void,
|
||||
getDom?(): HTMLElement,
|
||||
getStyles?(): string[],
|
||||
[key: string]: any,
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
register(moduleName: string, moduleProperties: ModuleProperties): void;
|
||||
};
|
||||
|
||||
export declare const Log: {
|
||||
info(message?: any, ...optionalParams: any[]): void,
|
||||
log(message?: any, ...optionalParams: any[]): void,
|
||||
error(message?: any, ...optionalParams: any[]): void,
|
||||
warn(message?: any, ...optionalParams: any[]): void,
|
||||
group(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupEnd(): void,
|
||||
time(timerName?: string): void,
|
||||
timeEnd(timerName?: string): void,
|
||||
timeStamp(timerName?: string): void,
|
||||
};
|
@@ -2,6 +2,42 @@
|
||||
|
||||
This document describes the way to develop your own MagicMirror² modules.
|
||||
|
||||
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 depends upon, including web links to those
|
||||
- Whether 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.
|
||||
@@ -10,11 +46,11 @@ A module can be placed in one single folder. Or multiple modules can be grouped
|
||||
|
||||
### Files
|
||||
- **modulename/modulename.js** - This is your core module script.
|
||||
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
|
||||
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
|
||||
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an integrated socket system.
|
||||
- **modulename/public** - Any files in this folder can be accessed 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 following 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.
|
||||
@@ -76,7 +98,7 @@ The data object contains additional metadata about the module instance:
|
||||
|
||||
|
||||
#### `defaults: {}`
|
||||
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
||||
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules' configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
||||
|
||||
#### `requiresVersion:`
|
||||
|
||||
@@ -112,7 +134,7 @@ loaded: function(callback) {
|
||||
````
|
||||
|
||||
#### `start()`
|
||||
This method is called when all modules are loaded an the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
||||
This method is called when all modules are loaded and the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -139,7 +161,7 @@ getScripts: function() {
|
||||
}
|
||||
|
||||
````
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external urls.
|
||||
|
||||
|
||||
#### `getStyles()`
|
||||
@@ -152,14 +174,14 @@ The getStyles method is called to request any additional stylesheets that need t
|
||||
getStyles: function() {
|
||||
return [
|
||||
'script.css', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
|
||||
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be avialable in the module folder.
|
||||
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be available in the module folder.
|
||||
this.file('anotherfile.css'), // this file will be loaded straight from the module folder.
|
||||
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', // this file will be loaded from the bootstrapcdn servers.
|
||||
]
|
||||
}
|
||||
|
||||
````
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external URLs.
|
||||
|
||||
#### `getTranslations()`
|
||||
**Should return: Dictionary**
|
||||
@@ -217,7 +239,7 @@ That MagicMirror core has the ability to send notifications to modules. Or even
|
||||
|
||||
- `notification` - String - The notification identifier.
|
||||
- `payload` - AnyType - The payload of a notification.
|
||||
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notififiction is the core system.
|
||||
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notification is the core system.
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -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)`
|
||||
@@ -323,7 +346,7 @@ Possible configurable options:
|
||||
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
|
||||
|
||||
|
||||
**Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
||||
**Note 1:** If the hide animation is cancelled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
||||
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
|
||||
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
||||
|
||||
@@ -348,7 +371,7 @@ Possible configurable options:
|
||||
|
||||
(*Introduced in version: 2.1.0.*)
|
||||
|
||||
Visiblity locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
||||
Visibility locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
||||
|
||||
**Module B asks module A to hide:**
|
||||
````javascript
|
||||
@@ -413,7 +436,7 @@ If no translation is found, a fallback will be used. The fallback sequence is as
|
||||
- 4. Translation as defined in core translation file of the fallback language (the first defined core translation file).
|
||||
- 5. The key (identifier) of the translation.
|
||||
|
||||
When adding translations to your module, it's a good idea to see if an apropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
||||
When adding translations to your module, it's a good idea to see if an appropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -467,7 +490,7 @@ this.translate("RUNNING", {
|
||||
)}); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended. (has a fallback)
|
||||
````
|
||||
|
||||
**Example swedish .json file that does not have the variable in it:**
|
||||
**Example Swedish .json file that does not have the variable in it:**
|
||||
````javascript
|
||||
{
|
||||
"RUNNING": "Slutar",
|
||||
@@ -555,6 +578,17 @@ start: function() {
|
||||
}
|
||||
````
|
||||
|
||||
#### `stop()`
|
||||
This method is called when the MagicMirror server receives a `SIGINT` command and is shutting down. This method should include any commands needed to close any open connections, stop any sub-processes and gracefully exit the module.
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
stop: function() {
|
||||
console.log("Shutting down MyModule");
|
||||
this.connection.close();
|
||||
}
|
||||
````
|
||||
|
||||
#### `socketNotificationReceived: function(notification, payload)`
|
||||
With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:
|
||||
|
||||
|
@@ -43,10 +43,11 @@ self.sendNotification("SHOW_ALERT", {});
|
||||
```
|
||||
|
||||
### Notification params
|
||||
| Option | Description
|
||||
| --------- | -----------
|
||||
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| Option | Description
|
||||
| ------------------ | -----------
|
||||
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `timer` (optional) | How long the notification should stay visible in ms. <br> If absent, the default `display_time` is used. <br> **Possible values:** `int` `float`
|
||||
|
||||
|
||||
### Alert params
|
||||
@@ -60,5 +61,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)
|
||||
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
||||
### [NotificationStyles](https://github.com/codrops/NotificationStyles)
|
||||
See [tympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
||||
|
@@ -24,7 +24,7 @@ Module.register("alert",{
|
||||
return ["classie.js", "modernizr.custom.js", "notificationFx.js"];
|
||||
},
|
||||
getStyles: function() {
|
||||
return ["ns-default.css"];
|
||||
return ["ns-default.css", "font-awesome.css"];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function() {
|
||||
@@ -38,20 +38,20 @@ Module.register("alert",{
|
||||
if (this.config.effect == "slide") {this.config.effect = this.config.effect + "-" + this.config.position;}
|
||||
msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin' style='line-height: 35px; font-size:24px' color='#4A4A4A'>" + message.title + "</span>";
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
}
|
||||
if (message.message){
|
||||
if (msg != ""){
|
||||
msg+= "<br />";
|
||||
}
|
||||
msg += "<span class='light' style='font-size:28px;line-height: 30px;'>" + message.message + "</span>";
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
}
|
||||
|
||||
new NotificationFx({
|
||||
message: msg,
|
||||
layout: "growl",
|
||||
effect: this.config.effect,
|
||||
ttl: this.config.display_time
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function(params, sender) {
|
||||
@@ -63,9 +63,9 @@ Module.register("alert",{
|
||||
params.imageUrl = null;
|
||||
image = "";
|
||||
} else if (typeof params.imageFA === "undefined"){
|
||||
image = "<img src='" + (params.imageUrl).toString() + "' height=" + (params.imageHeight).toString() + " style='margin-bottom: 10px;'/><br />";
|
||||
image = "<img src='" + (params.imageUrl).toString() + "' height='" + (params.imageHeight).toString() + "' style='margin-bottom: 10px;'/><br />";
|
||||
} else if (typeof params.imageUrl === "undefined"){
|
||||
image = "<span class='" + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;color: #fff;font-size:" + (params.imageHeight).toString() + ";'/></span><br />";
|
||||
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + (params.imageHeight).toString() + ";'/></span><br />";
|
||||
}
|
||||
//Create overlay
|
||||
var overlay = document.createElement("div");
|
||||
@@ -79,16 +79,16 @@ Module.register("alert",{
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
message ="";
|
||||
var message = "";
|
||||
if (params.title) {
|
||||
message += "<span class='light' style='line-height: 35px; font-size:30px' color='#4A4A4A'>" + params.title + "</span>"
|
||||
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||
}
|
||||
if (params.message) {
|
||||
if (message != ""){
|
||||
if (message !== ""){
|
||||
message += "<br />";
|
||||
}
|
||||
|
||||
message += "<span class='thin' style='font-size:22px;line-height: 30px;'>" + params.message + "</span>";
|
||||
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||
}
|
||||
|
||||
//Store alert in this.alerts
|
||||
@@ -110,25 +110,27 @@ Module.register("alert",{
|
||||
},
|
||||
hide_alert: function(sender) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
this.alerts[sender.name].dismiss();
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
var overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss();
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
var overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
},
|
||||
setPosition: function(pos) {
|
||||
//Add css to body depending on the set position for notifications
|
||||
var sheet = document.createElement("style");
|
||||
if (pos == "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";}
|
||||
if (pos == "right") {sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";}
|
||||
if (pos == "left") {sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";}
|
||||
if (pos === "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";}
|
||||
if (pos === "right") {sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";}
|
||||
if (pos === "left") {sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";}
|
||||
document.body.appendChild(sheet);
|
||||
|
||||
},
|
||||
notificationReceived: function(notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") { payload.type = "alert"; }
|
||||
if (payload.type == "alert") {
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type = "notification") {
|
||||
this.show_notification(payload);
|
||||
@@ -141,7 +143,7 @@ Module.register("alert",{
|
||||
this.alerts = {};
|
||||
this.setPosition(this.config.position);
|
||||
if (this.config.welcome_message) {
|
||||
if (this.config.welcome_message == true){
|
||||
if (this.config.welcome_message === true){
|
||||
this.show_notification({title: this.translate("sysTitle"), message: this.translate("welcome")});
|
||||
}
|
||||
else{
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* Based on work by http://tympanus.net/codrops/licensing/ */
|
||||
|
||||
.ns-box {
|
||||
background: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
@@ -12,7 +12,10 @@
|
||||
display: table;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.ns-alert {
|
||||
|
4
modules/default/alert/translations/bg.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror нотификация",
|
||||
"welcome": "Добре дошли, стартирането беше успешно"
|
||||
}
|
4
modules/default/alert/translations/fr.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||
}
|
20
modules/default/calendar/README.md
Normal file → Executable file
@@ -32,31 +32,41 @@ The following properties can be configured:
|
||||
| `defaultSymbol` | The default symbol. <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `calendar`
|
||||
| `maxTitleLength` | The maximum title length. <br><br> **Possible values:** `10` - `50` <br> **Default value:** `25`
|
||||
| `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `maxTitleLines` | The maximum number of lines a title will wrap vertically before being cut (Only enabled if `wrapEvents` is also enabled). <br><br> **Possible values:** `0` - `10` <br> **Default value:** `3`
|
||||
| `fetchInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `2000` (2 seconds)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `2000` (2 seconds)
|
||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
||||
| `tableClass` | Name of the classes issued from `main.css`. <br><br> **Possible values:** xsmall, small, medium, large, xlarge. <br> **Default value:** _small._
|
||||
| `calendars` | The list of calendars. <br><br> **Possible values:** An array, see _calendar configuration_ below. <br> **Default value:** _An example calendar._
|
||||
| `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }`
|
||||
| `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||
| `dateEndFormat` | Format to use for the end time of events <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `HH:mm` (e.g. 16:30)
|
||||
| `showEnd` | Show end time of events <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fullDayEventDateFormat` | Format to use for the date of full day events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||
| `timeFormat` | Display event times as absolute dates, or relative time <br><br> **Possible values:** `absolute` or `relative` <br> **Default value:** `relative`
|
||||
| `timeFormat` | Display event times as absolute dates, or relative time, or using absolute date headers with times for each event next to it <br><br> **Possible values:** `absolute` or `relative` or `dateheaders` <br> **Default value:** `relative`
|
||||
| `showEnd` | Display the end of a date as well <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6`
|
||||
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. 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:** `[]`
|
||||
| `hideOngoing` | Hides calendar events that have already started. <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>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>`regex` - set to `true` if filterBy is a regex. For those not familiar with regex it is used for pattern matching, please see [here](https://regexr.com/) for more info.<br><br> **Example:** `['Birthday', 'Hide This Event', {filterBy: 'Payment', until: '6 days', caseSensitive: true}, {filterBy: '^[0-9]{1,}.*', regex: true}]` <br> **Default value:** `[]`
|
||||
| `sliceMultiDayEvents` | If this is set to true, events exceeding at least one midnight will be sliced into separate events including a counter like (1/2). This is especially helpful in "dateheaders" mode. Events will be sliced at midnight, end time for all events but the last will be 23:59 **Default value:** `true`
|
||||
|
||||
|
||||
### Calendar configuration
|
||||
|
||||
The `calendars` property contains an array of the configured calendars.
|
||||
The `colored` property gives the option for an individual color for each calendar.
|
||||
The `coloredSymbolOnly` property will apply color to the symbol only, not the whole line. This is only applicable when `colored` is also enabled.
|
||||
|
||||
#### Default value:
|
||||
````javascript
|
||||
config: {
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
calendars: [
|
||||
{
|
||||
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
|
||||
@@ -80,7 +90,11 @@ config: {
|
||||
| `repeatingCountTitle` | The count title for yearly repating events in this calendar. <br><br> **Example:** `'Birthday'`
|
||||
| `maximumEntries` | The maximum number of events shown. Overrides global setting. **Possible values:** `0` - `100`
|
||||
| `maximumNumberOfDays` | The maximum number of days in the future. Overrides global setting
|
||||
| `name` | The name of the calendar. Included in event broadcasts as `calendarName`.
|
||||
| `auth` | The object containing options for authentication against the calendar.
|
||||
| `symbolClass` | Add a class to the cell of symbol.
|
||||
| `titleClass` | Add a class to the title's cell.
|
||||
| `timeClass` | Add a class to the time's cell.
|
||||
|
||||
|
||||
#### Calendar authentication options:
|
||||
|
391
modules/default/calendar/calendar.js
Normal file → Executable file
@@ -19,17 +19,23 @@ Module.register("calendar", {
|
||||
defaultRepeatingCountTitle: "",
|
||||
maxTitleLength: 25,
|
||||
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
||||
maxTitleLines: 3,
|
||||
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
|
||||
animationSpeed: 2000,
|
||||
fade: true,
|
||||
urgency: 7,
|
||||
timeFormat: "relative",
|
||||
dateFormat: "MMM Do",
|
||||
dateEndFormat: "LT",
|
||||
fullDayEventDateFormat: "MMM Do",
|
||||
showEnd: false,
|
||||
getRelative: 6,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar",
|
||||
@@ -41,7 +47,8 @@ Module.register("calendar", {
|
||||
"'s birthday": ""
|
||||
},
|
||||
broadcastEvents: true,
|
||||
excludedEvents: []
|
||||
excludedEvents: [],
|
||||
sliceMultiDayEvents: false
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -77,6 +84,15 @@ Module.register("calendar", {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays
|
||||
};
|
||||
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
calendarConfig.symbolClass = "";
|
||||
}
|
||||
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
||||
calendarConfig.titleClass = "";
|
||||
}
|
||||
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
||||
calendarConfig.timeClass = "";
|
||||
}
|
||||
|
||||
// we check user and password here for backwards compatibility with old configs
|
||||
if(calendar.user && calendar.pass) {
|
||||
@@ -89,6 +105,13 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
|
||||
// Trigger ADD_CALENDAR every fetchInterval to make sure there is always a calendar
|
||||
// fetcher running on the server side.
|
||||
var self = this;
|
||||
setInterval(function() {
|
||||
self.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
}, self.config.fetchInterval);
|
||||
}
|
||||
|
||||
this.calendarData = {};
|
||||
@@ -122,19 +145,52 @@ Module.register("calendar", {
|
||||
|
||||
var events = this.createEventList();
|
||||
var wrapper = document.createElement("table");
|
||||
wrapper.className = "small";
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (events.length === 0) {
|
||||
wrapper.innerHTML = (this.loaded) ? this.translate("EMPTY") : this.translate("LOADING");
|
||||
wrapper.className = "small dimmed";
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startFade = events.length * this.config.fadePoint;
|
||||
var fadeSteps = events.length - startFade;
|
||||
}
|
||||
|
||||
var currentFadeStep = 0;
|
||||
var lastSeenDate = "";
|
||||
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
if(this.config.timeFormat === "dateheaders"){
|
||||
if(lastSeenDate !== dateAsString){
|
||||
var dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
var dateCell = document.createElement("td");
|
||||
|
||||
dateCell.colSpan = "3";
|
||||
dateCell.innerHTML = dateAsString;
|
||||
dateRow.appendChild(dateCell);
|
||||
wrapper.appendChild(dateRow);
|
||||
|
||||
if (e >= startFade) { //fading
|
||||
currentFadeStep = e - startFade;
|
||||
dateRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||
}
|
||||
|
||||
lastSeenDate = dateAsString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored) {
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
@@ -142,7 +198,14 @@ Module.register("calendar", {
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolWrapper = document.createElement("td");
|
||||
symbolWrapper.className = "symbol align-right";
|
||||
|
||||
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
var symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
var symbols = this.symbolsForUrl(event.url);
|
||||
if(typeof symbols === "string") {
|
||||
symbols = [symbols];
|
||||
@@ -150,19 +213,23 @@ Module.register("calendar", {
|
||||
|
||||
for(var i = 0; i < symbols.length; i++) {
|
||||
var symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-" + symbols[i];
|
||||
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||
if(i > 0){
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
}
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
}else if(this.config.timeFormat === "dateheaders"){
|
||||
var blankCell = document.createElement("td");
|
||||
blankCell.innerHTML = " ";
|
||||
eventWrapper.appendChild(blankCell);
|
||||
}
|
||||
|
||||
var titleWrapper = document.createElement("td"),
|
||||
repeatingCountTitle = "";
|
||||
|
||||
if (this.config.displayRepeatingCountTitle) {
|
||||
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
||||
|
||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||
|
||||
@@ -176,109 +243,141 @@ Module.register("calendar", {
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
|
||||
|
||||
var titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright";
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
} else {
|
||||
titleWrapper.className = "title";
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
if(this.config.timeFormat === "dateheaders"){
|
||||
|
||||
var timeWrapper = document.createElement("td");
|
||||
//console.log(event.today);
|
||||
var now = new Date();
|
||||
// Define second, minute, hour, and day variables
|
||||
var oneSecond = 1000; // 1,000 milliseconds
|
||||
var oneMinute = oneSecond * 60;
|
||||
var oneHour = oneMinute * 60;
|
||||
var oneDay = oneHour * 24;
|
||||
if (event.fullDayEvent) {
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
|
||||
}else{
|
||||
|
||||
var timeClass = this.timeClassForUrl(event.url);
|
||||
var timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light " + timeClass;
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.align = "right";
|
||||
}
|
||||
} else {
|
||||
if (event.startDate >= new Date()) {
|
||||
if (event.startDate - now < 2 * oneDay) {
|
||||
// This event is within the next 48 hours (2 days)
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
}else{
|
||||
var timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
//console.log(event.today);
|
||||
var now = new Date();
|
||||
// Define second, minute, hour, and day variables
|
||||
var oneSecond = 1000; // 1,000 milliseconds
|
||||
var oneMinute = oneSecond * 60;
|
||||
var oneHour = oneMinute * 60;
|
||||
var oneDay = oneHour * 24;
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= oneSecond;
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
} else {
|
||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
}
|
||||
}
|
||||
if(this.config.showEnd){
|
||||
timeWrapper.innerHTML += "-" ;
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate , "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
if (event.startDate >= new Date()) {
|
||||
if (event.startDate - now < 2 * oneDay) {
|
||||
// This event is within the next 48 hours (2 days)
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
if(this.config.timeFormat === "absolute") {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
} else {
|
||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
|
||||
}
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
//console.log(event);
|
||||
var timeClass = this.timeClassForUrl(event.url);
|
||||
timeWrapper.className = "time light " + timeClass;
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
//console.log(event);
|
||||
timeWrapper.className = "time light";
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startingPoint = events.length * this.config.fadePoint;
|
||||
var steps = events.length - startingPoint;
|
||||
if (e >= startingPoint) {
|
||||
var currentStep = e - startingPoint;
|
||||
eventWrapper.style.opacity = 1 - (1 / steps * currentStep);
|
||||
}
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,9 +388,9 @@ Module.register("calendar", {
|
||||
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
|
||||
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
|
||||
* it will a localeSpecification object with the system locale time format.
|
||||
*
|
||||
*
|
||||
* @param {number} timeFormat Specifies either 12 or 24 hour time format
|
||||
* @returns {moment.LocaleSpecification}
|
||||
* @returns {moment.LocaleSpecification}
|
||||
*/
|
||||
getLocaleSpecification: function(timeFormat) {
|
||||
switch (timeFormat) {
|
||||
@@ -336,6 +435,7 @@ Module.register("calendar", {
|
||||
createEventList: function () {
|
||||
var events = [];
|
||||
var today = moment().startOf("day");
|
||||
var now = new Date();
|
||||
for (var c in this.calendarData) {
|
||||
var calendar = this.calendarData[c];
|
||||
for (var e in calendar) {
|
||||
@@ -346,9 +446,41 @@ Module.register("calendar", {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(this.config.hideOngoing) {
|
||||
if(event.startDate < now) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(this.listContainsEvent(events,event)){
|
||||
continue;
|
||||
}
|
||||
event.url = c;
|
||||
event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000);
|
||||
events.push(event);
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
if (this.config.sliceMultiDayEvents) {
|
||||
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x"); //next midnight
|
||||
var count = 1;
|
||||
var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1
|
||||
if (event.endDate > midnight) {
|
||||
while (event.endDate > midnight) {
|
||||
var nextEvent = JSON.parse(JSON.stringify(event)); //make a copy without reference to the original event
|
||||
nextEvent.startDate = midnight;
|
||||
event.endDate = midnight;
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
events.push(event);
|
||||
event = nextEvent;
|
||||
count += 1;
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); //move further one day for next split
|
||||
}
|
||||
event.title += " ("+count+"/"+maxCount+")";
|
||||
}
|
||||
events.push(event);
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +488,18 @@ Module.register("calendar", {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return events;
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
|
||||
listContainsEvent: function(eventList, event){
|
||||
for(var evt of eventList){
|
||||
if(evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
},
|
||||
|
||||
/* createEventList(url)
|
||||
@@ -371,11 +514,15 @@ Module.register("calendar", {
|
||||
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
||||
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
||||
fetchInterval: this.config.fetchInterval,
|
||||
symbolClass: calendarConfig.symbolClass,
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
auth: auth
|
||||
});
|
||||
},
|
||||
|
||||
/* symbolsForUrl(url)
|
||||
/**
|
||||
* symbolsForUrl(url)
|
||||
* Retrieves the symbols for a specific url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
@@ -386,6 +533,53 @@ Module.register("calendar", {
|
||||
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolClassForUrl(url)
|
||||
* Retrieves the symbolClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
symbolClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbolClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* titleClassForUrl(url)
|
||||
* Retrieves the titleClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
titleClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "titleClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* timeClassForUrl(url)
|
||||
* Retrieves the timeClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
timeClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "timeClass", "");
|
||||
},
|
||||
|
||||
/* calendarNameForUrl(url)
|
||||
* Retrieves the calendar name for a specific url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The name of the calendar
|
||||
*/
|
||||
calendarNameForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "name", "");
|
||||
},
|
||||
|
||||
/* colorForUrl(url)
|
||||
* Retrieves the color for a specific url.
|
||||
*
|
||||
@@ -430,13 +624,14 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
|
||||
*
|
||||
*
|
||||
* @param {string} string Text string to shorten
|
||||
* @param {number} maxLength The max length of the string
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The shortened string
|
||||
*/
|
||||
shorten: function (string, maxLength, wrapEvents) {
|
||||
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
@@ -445,12 +640,21 @@ Module.register("calendar", {
|
||||
var temp = "";
|
||||
var currentLine = "";
|
||||
var words = string.split(" ");
|
||||
var line = 0;
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
var word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space
|
||||
currentLine += (word + " ");
|
||||
} else {
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentLine.length > 0) {
|
||||
temp += (currentLine + "<br>" + word + " ");
|
||||
} else {
|
||||
@@ -501,7 +705,7 @@ Module.register("calendar", {
|
||||
title = title.replace(needle, replacement);
|
||||
}
|
||||
|
||||
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents);
|
||||
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines);
|
||||
return title;
|
||||
},
|
||||
|
||||
@@ -516,6 +720,7 @@ Module.register("calendar", {
|
||||
for (var e in calendar) {
|
||||
var event = cloneObject(calendar[e]);
|
||||
event.symbol = this.symbolsForUrl(url);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
delete event.url;
|
||||
eventList.push(event);
|
||||
|
@@ -29,7 +29,8 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
}
|
||||
},
|
||||
gzip: true
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
@@ -90,6 +91,9 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var endDate;
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if(typeof event.duration !== "undefined") {
|
||||
dur=moment.duration(event.duration);
|
||||
endDate = startDate.clone().add(dur);
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
endDate = startDate;
|
||||
@@ -113,11 +117,48 @@ 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,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// 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 if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -130,13 +171,26 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var geo = event.geo || false;
|
||||
var description = event.description || false;
|
||||
|
||||
if (typeof event.rrule != "undefined" && !isFacebookBirthday) {
|
||||
if (typeof event.rrule != "undefined" && event.rrule != null && !isFacebookBirthday) {
|
||||
var rule = event.rrule;
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if(rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900 ||
|
||||
rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900){
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
var dates = rule.between(today, future, true, limitFunction);
|
||||
|
||||
for (var d in dates) {
|
||||
startDate = moment(new Date(dates[d]));
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endDate.format("x") > now) {
|
||||
newEvents.push({
|
||||
title: title,
|
||||
@@ -171,6 +225,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({
|
||||
@@ -215,7 +273,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
/* isFullDayEvent(event)
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* argument event obejct - The event object to check.
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
*/
|
||||
@@ -227,8 +285,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var start = event.start || 0;
|
||||
var startDate = new Date(start);
|
||||
var end = event.end || 0;
|
||||
|
||||
if (end - start === 24 * 60 * 60 * 1000 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
if (((end - start) % (24 * 60 * 60 * 1000)) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
}
|
||||
@@ -236,6 +293,44 @@ 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;
|
||||
};
|
||||
|
||||
var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
filter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
|
||||
filter = new RegExp(filter, regexFlags);
|
||||
|
||||
return filter.test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* startFetch()
|
||||
|
78
modules/default/calendar/vendor/ical.js/ical.js
vendored
@@ -80,16 +80,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
var addTZ = function(dt, name, params){
|
||||
var addTZ = function(dt, params){
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
dt[name].tz = p.TZID
|
||||
if (params && p && dt){
|
||||
dt.tz = p.TZID
|
||||
}
|
||||
|
||||
return dt
|
||||
}
|
||||
|
||||
var parseTimestamp = function(val){
|
||||
//typical RFC date-time format
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||
if (comps !== null) {
|
||||
if (comps[7] == 'Z'){ // GMT
|
||||
return new Date(Date.UTC(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10 )
|
||||
));
|
||||
// TODO add tz
|
||||
} else {
|
||||
return new Date(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var dateParam = function(name){
|
||||
return function(val, params, curr){
|
||||
|
||||
@@ -108,37 +137,24 @@
|
||||
comps[3]
|
||||
);
|
||||
|
||||
return addTZ(curr, name, params);
|
||||
curr[name] = addTZ(curr[name], params);
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
curr[name] = []
|
||||
val.split(',').forEach(function(val){
|
||||
var newDate = parseTimestamp(val);
|
||||
curr[name].push(addTZ(newDate, params));
|
||||
});
|
||||
|
||||
//typical RFC date-time format
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||
if (comps !== null) {
|
||||
if (comps[7] == 'Z'){ // GMT
|
||||
curr[name] = new Date(Date.UTC(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10 )
|
||||
));
|
||||
// TODO add tz
|
||||
} else {
|
||||
curr[name] = new Date(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10)
|
||||
);
|
||||
}
|
||||
if (curr[name].length === 0){
|
||||
delete curr[name];
|
||||
} else if (curr[name].length === 1){
|
||||
curr[name] = curr[name][0];
|
||||
}
|
||||
|
||||
return addTZ(curr, name, params)
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +164,11 @@
|
||||
if (date.exdates === undefined) {
|
||||
date.exdates = [];
|
||||
}
|
||||
date.exdates.push(date.exdate);
|
||||
if (Array.isArray(date.exdate)){
|
||||
date.exdates = date.exdates.concat(date.exdate);
|
||||
} else {
|
||||
date.exdates.push(date.exdate);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,13 @@ ical.objectHandlers['END'] = function(val, params, curr, stack){
|
||||
rule += ' EXDATE:' + curr.exdates[i].toISOString().replace(/[-:]/g, '');
|
||||
rule = rule.replace(/\.[0-9]{3}/, '');
|
||||
}
|
||||
curr.rrule = rrulestr(rule);
|
||||
try {
|
||||
curr.rrule = rrulestr(rule);
|
||||
}
|
||||
catch(err) {
|
||||
console.log("Unrecognised element in calendar feed, ignoring: " + rule);
|
||||
curr.rrule = null;
|
||||
}
|
||||
}
|
||||
return originalEnd.call(this, val, params, curr, stack);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -2,6 +2,11 @@
|
||||
The `clock` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current date and time. The information will be updated realtime.
|
||||
|
||||
## Screenshot
|
||||
|
||||
- Current time
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
|
@@ -94,7 +94,7 @@ Module.register("clock",{
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
}
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK") + " " + now.week();
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
}
|
||||
timeWrapper.innerHTML = timeString;
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
@@ -137,7 +137,8 @@ Module.register("clock",{
|
||||
clockCircle.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
clockCircle.style.border = "1px solid black";
|
||||
// clockCircle.style.border = "1px solid black";
|
||||
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
|
||||
} else if (this.config.analogFace != "none") {
|
||||
clockCircle.style.border = "2px solid white";
|
||||
|
BIN
modules/default/clock/clock_screenshot.png
Normal file
After Width: | Height: | Size: 30 KiB |
@@ -2,6 +2,10 @@
|
||||
The `compliments` module is one of the default modules of the MagicMirror.
|
||||
This module displays a random compliment.
|
||||
|
||||
## Screenshots
|
||||
- Compliments Screenshot
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -30,8 +34,14 @@ The following properties can be configured:
|
||||
| `updateInterval` | How often does the compliment have to change? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `30000` (30 seconds)
|
||||
| `fadeSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `4000` (4 seconds)
|
||||
| `compliments` | The list of compliments. <br><br> **Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below. <br> **Default value:** See _compliment configuration_ below.
|
||||
| `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json` <br> **Default value:** `null` (Do not load from file)
|
||||
| `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path or URL (starting with `http://` or `https://`) 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
|
||||
|
||||
@@ -39,22 +49,22 @@ The `compliments` property contains an object with four arrays: <code>morning</c
|
||||
|
||||
|
||||
If use the currentweather is possible use a actual weather for set compliments. The availables properties are:
|
||||
* `day_sunny`
|
||||
* `day_cloudy`
|
||||
* `cloudy`
|
||||
* `cloudy_windy`
|
||||
* `showers`
|
||||
* `rain`
|
||||
* `thunderstorm`
|
||||
* `snow`
|
||||
* `fog`
|
||||
* `night_clear`
|
||||
* `night_cloudy`
|
||||
* `night_showers`
|
||||
* `night_rain`
|
||||
* `night_thunderstorm`
|
||||
* `night_snow`
|
||||
* `night_alt_cloudy_windy`
|
||||
- `day_sunny`
|
||||
- `day_cloudy`
|
||||
- `cloudy`
|
||||
- `cloudy_windy`
|
||||
- `showers`
|
||||
- `rain`
|
||||
- `thunderstorm`
|
||||
- `snow`
|
||||
- `fog`
|
||||
- `night_clear`
|
||||
- `night_cloudy`
|
||||
- `night_showers`
|
||||
- `night_rain`
|
||||
- `night_thunderstorm`
|
||||
- `night_snow`
|
||||
- `night_alt_cloudy_windy`
|
||||
|
||||
#### Example use with currentweather module
|
||||
````javascript
|
||||
@@ -101,6 +111,13 @@ config: {
|
||||
}
|
||||
````
|
||||
|
||||
#### Multi-line compliments:
|
||||
Use `\n` to split compliment text into multiple lines, e.g. `First line.\nSecond line.` will be shown as:
|
||||
```
|
||||
First line.
|
||||
Second line.
|
||||
```
|
||||
|
||||
### External Compliment File
|
||||
You may specify an external file that contains the three compliment arrays. This is particularly useful if you have a
|
||||
large number of compliments and do not wish to crowd your `config.js` file with a large array of compliments.
|
||||
|
@@ -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);
|
||||
this.complimentFile(function(response) {
|
||||
self.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);
|
||||
@@ -123,9 +128,11 @@ Module.register("compliments", {
|
||||
* Retrieve a file from the local filesystem
|
||||
*/
|
||||
complimentFile: function(callback) {
|
||||
var xobj = new XMLHttpRequest();
|
||||
var xobj = new XMLHttpRequest(),
|
||||
isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
||||
path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
||||
xobj.overrideMimeType("application/json");
|
||||
xobj.open("GET", this.file(this.config.remoteFile), true);
|
||||
xobj.open("GET", path, true);
|
||||
xobj.onreadystatechange = function() {
|
||||
if (xobj.readyState == 4 && xobj.status == "200") {
|
||||
callback(xobj.responseText);
|
||||
@@ -152,7 +159,7 @@ Module.register("compliments", {
|
||||
|
||||
var compliment = document.createTextNode(complimentText);
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright";
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||
wrapper.appendChild(compliment);
|
||||
|
||||
return wrapper;
|
||||
@@ -192,4 +199,4 @@ Module.register("compliments", {
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
BIN
modules/default/compliments/compliments_screenshot.png
Normal file
After Width: | Height: | Size: 26 KiB |
@@ -2,6 +2,11 @@
|
||||
The `currentweather` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.
|
||||
|
||||
## Screenshot
|
||||
|
||||
- Current weather screenshot
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -29,7 +34,7 @@ The following properties can be configured:
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](http://openweathermap.org/help/city_list.txt) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `appid` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` =Fahrenheit <br> **Default value:** `config.units`
|
||||
| `roundTemp` | Round temperature value to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
@@ -43,9 +48,12 @@ 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:** `.`
|
||||
| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds) <br><br> **Possible values:** `1000` - `5000` <br> **Default value:** `0`
|
||||
| `retryDelay` | The delay before retrying after a request failure. (Milliseconds) <br><br> **Possible values:** `1000` - `60000` <br> **Default value:** `2500`
|
||||
| `apiVersion` | The OpenWeatherMap API version to use. <br><br> **Default value:** `2.5`
|
||||
|
@@ -23,17 +23,20 @@ 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,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "http://api.openweathermap.org/data/",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
@@ -64,11 +67,11 @@ Module.register("currentweather",{
|
||||
},
|
||||
},
|
||||
|
||||
// create a variable for the first upcoming calendaar event. Used if no location is specified.
|
||||
// create a variable for the first upcoming calendar event. Used if no location is specified.
|
||||
firstEvent: false,
|
||||
|
||||
// create a variable to hold the location name based on the API result.
|
||||
fetchedLocatioName: "",
|
||||
fetchedLocationName: "",
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function() {
|
||||
@@ -84,7 +87,7 @@ Module.register("currentweather",{
|
||||
getTranslations: function() {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefor we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build yiur own module including translations, check out the documentation.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -104,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);
|
||||
|
||||
@@ -195,23 +198,30 @@ Module.register("currentweather",{
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.degreeLabel) {
|
||||
switch (this.config.units ) {
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if(this.config.degreeLabel) {
|
||||
switch(this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel = "C";
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel = "F";
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel = "K";
|
||||
degreeLabel += "K";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.decimalSymbol === "") {
|
||||
this.config.decimalSymbol = ".";
|
||||
}
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
temperature.className = "bright";
|
||||
temperature.innerHTML = " " + this.temperature + "°" + degreeLabel;
|
||||
temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(temperature);
|
||||
|
||||
if (this.config.showIndoorTemperature && this.indoorTemperature) {
|
||||
@@ -221,7 +231,7 @@ Module.register("currentweather",{
|
||||
|
||||
var indoorTemperatureElem = document.createElement("span");
|
||||
indoorTemperatureElem.className = "bright";
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature + "°" + degreeLabel;
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(indoorTemperatureElem);
|
||||
}
|
||||
|
||||
@@ -237,13 +247,26 @@ 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 = this.translate("FEELS") + " " + this.feelsLike + degreeLabel;
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function() {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
return this.data.header + " " + this.fetchedLocatioName;
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined) {
|
||||
return this.data.header + " " + this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
@@ -273,11 +296,11 @@ Module.register("currentweather",{
|
||||
}
|
||||
if (notification === "INDOOR_TEMPERATURE") {
|
||||
this.indoorTemperature = this.roundValue(payload);
|
||||
this.updateDom(self.config.animationSpeed);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(self.config.animationSpeed);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -360,13 +383,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 = parseFloat((Hindex - 32) / 1.8).toFixed(0);
|
||||
break;
|
||||
case "imperial": this.feelsLike = Hindex.toFixed(0);
|
||||
break;
|
||||
case "default":
|
||||
var tc = parseFloat((Hindex - 32) / 1.8) + 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];
|
||||
@@ -492,4 +573,5 @@ Module.register("currentweather",{
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
}
|
||||
|
||||
});
|
||||
|
BIN
modules/default/currentweather/weather_screenshot.png
Normal file
After Width: | Height: | Size: 38 KiB |
@@ -16,7 +16,8 @@ var defaultModules = [
|
||||
"helloworld",
|
||||
"newsfeed",
|
||||
"weatherforecast",
|
||||
"updatenotification"
|
||||
"updatenotification",
|
||||
"weather"
|
||||
];
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -14,10 +14,11 @@ Module.register("helloworld",{
|
||||
text: "Hello World!"
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function() {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = this.config.text;
|
||||
return wrapper;
|
||||
getTemplate: function () {
|
||||
return "helloworld.njk"
|
||||
},
|
||||
|
||||
getTemplateData: function () {
|
||||
return this.config
|
||||
}
|
||||
});
|
||||
|
5
modules/default/helloworld/helloworld.njk
Normal file
@@ -0,0 +1,5 @@
|
||||
<!--
|
||||
Use ` | safe` to allow html tages within the text string.
|
||||
https://mozilla.github.io/nunjucks/templating.html#autoescaping
|
||||
-->
|
||||
<div>{{text | safe}}</div>
|
@@ -2,6 +2,10 @@
|
||||
The `newsfeed ` module is one of the default modules of the MagicMirror.
|
||||
This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (````updateInterval````), but can also be controlled by sending news feed specific notifications to the module.
|
||||
|
||||
## Screenshot
|
||||
- News Feed Screenshot using the NYT
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
### Configuration
|
||||
@@ -39,8 +43,10 @@ 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.
|
||||
| `ARTICLE_TOGGLE_FULL` | Toogles article in fullscreen.
|
||||
| `ARTICLE_INFO_REQUEST` | Causes `newsfeed` to respond with the notification `ARTICLE_INFO_RESPONSE`, the payload of which provides the `title`, `source`, `date`, `desc` and `url` of the current news title.
|
||||
|
||||
Note the payload of the sent notification event is ignored.
|
||||
|
||||
@@ -57,25 +63,30 @@ The third party [MMM-Gestures](https://github.com/thobach/MMM-Gestures) module s
|
||||
|
||||
The following properties can be configured:
|
||||
|
||||
| Option | Description
|
||||
| ----------------- | -----------
|
||||
| `feeds` | An array of feed urls that will be used as source. <br> More info about this object can be found below. <br> **Default value:** `[{ title: "New York Times", url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", encoding: "UTF-8" }]`
|
||||
| `showSourceTitle` | Display the title of the source. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showPublishDate` | Display the publish date of an headline. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showDescription` | Display the description of an item. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `wrapTitle` | Wrap the title of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `wrapDescription` | Wrap the description of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `hideLoading` | Hide module instead of showing LOADING status. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `reloadInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
||||
| `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`
|
||||
| `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',...]`
|
||||
| Option | Description
|
||||
| ------------------ | -----------
|
||||
| `feeds` | An array of feed urls that will be used as source. <br> More info about this object can be found below. <br> **Default value:** `[{ title: "New York Times", url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", encoding: "UTF-8" }]`<br>You can add `reloadInterval` option to set particular reloadInterval to a feed.
|
||||
| `showSourceTitle` | Display the title of the source. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showPublishDate` | Display the publish date of an headline. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showDescription` | Display the description of an item. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `wrapTitle` | Wrap the title of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `wrapDescription` | Wrap the description of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `truncDescription` | Truncate description? <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `lengthDescription`| How many characters to be displayed for a truncated description? <br><br> **Possible values:** `1` - `500` <br> **Default value:** `400`
|
||||
| `hideLoading` | Hide module instead of showing LOADING status. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `reloadInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
||||
| `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`
|
||||
| `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`
|
||||
| `logFeedWarnings` | Log warnings when there is an error parsing a news article. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
|
||||
The `feeds` property contains an array with multiple objects. These objects have the following properties:
|
||||
|
||||
|
@@ -14,9 +14,10 @@ var iconv = require("iconv-lite");
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article.
|
||||
*/
|
||||
|
||||
var Fetcher = function(url, reloadInterval, encoding) {
|
||||
var Fetcher = function(url, reloadInterval, encoding, logFeedWarnings) {
|
||||
var self = this;
|
||||
if (reloadInterval < 1000) {
|
||||
reloadInterval = 1000;
|
||||
@@ -45,13 +46,13 @@ 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) {
|
||||
|
||||
var regex = /(<([^>]+)>)/ig;
|
||||
description = description.replace(regex, "");
|
||||
description = description.toString().replace(regex, "");
|
||||
|
||||
items.push({
|
||||
title: title,
|
||||
@@ -60,18 +61,17 @@ var Fetcher = function(url, reloadInterval, encoding) {
|
||||
url: url,
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// console.log("Can't parse feed item:");
|
||||
// console.log(item);
|
||||
// console.log('Title: ' + title);
|
||||
// console.log('Description: ' + description);
|
||||
// console.log('Pubdate: ' + pubdate);
|
||||
|
||||
} else if (logFeedWarnings) {
|
||||
console.log("Can't parse feed item:");
|
||||
console.log(item);
|
||||
console.log("Title: " + title);
|
||||
console.log("Description: " + description);
|
||||
console.log("Pubdate: " + pubdate);
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", function() {
|
||||
parser.on("end", function() {
|
||||
//console.log("end parsing - " + url);
|
||||
self.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
@@ -83,7 +83,9 @@ var Fetcher = function(url, reloadInterval, encoding) {
|
||||
|
||||
|
||||
nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
headers = {"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"}
|
||||
headers = {"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache"}
|
||||
|
||||
request({uri: url, encoding: null, headers: headers})
|
||||
.on("error", function(error) {
|
||||
|
@@ -23,8 +23,10 @@ Module.register("newsfeed",{
|
||||
showDescription: false,
|
||||
wrapTitle: true,
|
||||
wrapDescription: true,
|
||||
truncDescription: true,
|
||||
lengthDescription: 400,
|
||||
hideLoading: false,
|
||||
reloadInterval: 5 * 60 * 1000, // every 5 minutes
|
||||
reloadInterval: 5 * 60 * 1000, // every 5 minutes
|
||||
updateInterval: 10 * 1000,
|
||||
animationSpeed: 2.5 * 1000,
|
||||
maxNewsItems: 0, // 0 for unlimited
|
||||
@@ -33,8 +35,10 @@ Module.register("newsfeed",{
|
||||
removeStartTags: "",
|
||||
removeEndTags: "",
|
||||
startTags: [],
|
||||
endTags: []
|
||||
|
||||
endTags: [],
|
||||
prohibitedWords: [],
|
||||
scrollLength: 500,
|
||||
logFeedWarnings: false
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -60,9 +64,11 @@ Module.register("newsfeed",{
|
||||
this.newsItems = [];
|
||||
this.loaded = false;
|
||||
this.activeItem = 0;
|
||||
this.scrollPosition = 0;
|
||||
|
||||
this.registerFeeds();
|
||||
|
||||
this.isShowingDescription = this.config.showDescription;
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
@@ -84,7 +90,7 @@ Module.register("newsfeed",{
|
||||
|
||||
if (this.config.feedUrl) {
|
||||
wrapper.className = "small bright";
|
||||
wrapper.innerHTML = "The configuration options for the newsfeed module have changed.<br>Please check the documentation.";
|
||||
wrapper.innerHTML = this.translate("configuration_changed");
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -117,22 +123,22 @@ Module.register("newsfeed",{
|
||||
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeStartTags == "title" || this.config.removeStartTags == "both") {
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
|
||||
for (f=0; f<this.config.startTags.length;f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
|
||||
if (this.newsItems[this.activeItem].title.slice(0,this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].title.length);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (this.config.removeStartTags == "description" || this.config.removeStartTags == "both") {
|
||||
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
||||
|
||||
if (this.config.showDescription) {
|
||||
if (this.isShowingDescription) {
|
||||
for (f=0; f<this.config.startTags.length;f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
|
||||
if (this.newsItems[this.activeItem].description.slice(0,this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,14 +149,14 @@ Module.register("newsfeed",{
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (f=0; f<this.config.endTags.length;f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
|
||||
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length)===this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0,-this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.showDescription) {
|
||||
if (this.isShowingDescription) {
|
||||
for (f=0; f<this.config.endTags.length;f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
|
||||
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length)===this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0,-this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
@@ -165,23 +171,26 @@ Module.register("newsfeed",{
|
||||
wrapper.appendChild(title);
|
||||
}
|
||||
|
||||
if (this.config.showDescription) {
|
||||
if (this.isShowingDescription) {
|
||||
var description = document.createElement("div");
|
||||
description.className = "small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||
description.innerHTML = this.newsItems[this.activeItem].description;
|
||||
var txtDesc = this.newsItems[this.activeItem].description;
|
||||
description.innerHTML = (this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc);
|
||||
wrapper.appendChild(description);
|
||||
}
|
||||
|
||||
if (this.config.showFullArticle) {
|
||||
var fullArticle = document.createElement("iframe");
|
||||
fullArticle.className = "";
|
||||
fullArticle.style.width = "100%";
|
||||
fullArticle.style.width = "100vw";
|
||||
// very large height value to allow scrolling
|
||||
fullArticle.height = "3000";
|
||||
fullArticle.style.height = "3000";
|
||||
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 = this.getActiveItemURL()
|
||||
fullArticle.style.zIndex = 1;
|
||||
wrapper.appendChild(fullArticle);
|
||||
}
|
||||
|
||||
@@ -201,6 +210,10 @@ Module.register("newsfeed",{
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
getActiveItemURL: function() {
|
||||
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||
},
|
||||
|
||||
/* registerFeeds()
|
||||
* registers the feeds to be used by the backend.
|
||||
*/
|
||||
@@ -241,6 +254,18 @@ Module.register("newsfeed",{
|
||||
if(this.config.maxNewsItems > 0) {
|
||||
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
||||
}
|
||||
|
||||
if(this.config.prohibitedWords.length > 0) {
|
||||
newsItems = newsItems.filter(function(value){
|
||||
for (var i=0; i < this.config.prohibitedWords.length; i++) {
|
||||
if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, this);
|
||||
}
|
||||
|
||||
this.newsItems = newsItems;
|
||||
},
|
||||
|
||||
@@ -304,8 +329,12 @@ Module.register("newsfeed",{
|
||||
},
|
||||
|
||||
resetDescrOrFullArticleAndTimer: function() {
|
||||
this.config.showDescription = false;
|
||||
this.isShowingDescription = this.config.showDescription;
|
||||
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();
|
||||
}
|
||||
@@ -313,7 +342,7 @@ Module.register("newsfeed",{
|
||||
|
||||
notificationReceived: function(notification, payload, sender) {
|
||||
Log.info(this.name + " - received notification: " + notification);
|
||||
if(notification == "ARTICLE_NEXT"){
|
||||
if(notification === "ARTICLE_NEXT"){
|
||||
var before = this.activeItem;
|
||||
this.activeItem++;
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
@@ -322,7 +351,7 @@ Module.register("newsfeed",{
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
this.updateDom(100);
|
||||
} else if(notification == "ARTICLE_PREVIOUS"){
|
||||
} else if(notification === "ARTICLE_PREVIOUS"){
|
||||
var before = this.activeItem;
|
||||
this.activeItem--;
|
||||
if (this.activeItem < 0) {
|
||||
@@ -333,20 +362,60 @@ Module.register("newsfeed",{
|
||||
this.updateDom(100);
|
||||
}
|
||||
// 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);
|
||||
} else if(notification == "ARTICLE_LESS_DETAILS"){
|
||||
else if(notification === "ARTICLE_MORE_DETAILS"){
|
||||
// full article is already showing, so scrolling down
|
||||
if(this.config.showFullArticle === true){
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling down");
|
||||
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
}
|
||||
else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
} else if(notification === "ARTICLE_SCROLL_UP"){
|
||||
if(this.config.showFullArticle === true){
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling up");
|
||||
Log.info(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
}
|
||||
} else if(notification === "ARTICLE_LESS_DETAILS"){
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - showing only article titles again");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL"){
|
||||
if (this.config.showFullArticle){
|
||||
this.activeItem++;
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
} else if (notification === "ARTICLE_INFO_REQUEST"){
|
||||
this.sendNotification("ARTICLE_INFO_RESPONSE", {
|
||||
title: this.newsItems[this.activeItem].title,
|
||||
source: this.newsItems[this.activeItem].sourceTitle,
|
||||
date: this.newsItems[this.activeItem].pubdate,
|
||||
desc: this.newsItems[this.activeItem].description,
|
||||
url: this.getActiveItemURL()
|
||||
})
|
||||
} else {
|
||||
Log.info(this.name + " - unknown notification, ignoring: " + notification);
|
||||
}
|
||||
},
|
||||
|
||||
showFullArticle: function() {
|
||||
this.isShowingDescription = !this.isShowingDescription;
|
||||
this.config.showFullArticle = !this.isShowingDescription;
|
||||
// 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.isShowingDescription ? "article description" : "full article");
|
||||
this.updateDom(100);
|
||||
}
|
||||
|
||||
});
|
||||
|
BIN
modules/default/newsfeed/newsfeed_screenshot.png
Normal file
After Width: | Height: | Size: 44 KiB |
@@ -36,7 +36,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
var url = feed.url || "";
|
||||
var encoding = feed.encoding || "UTF-8";
|
||||
var reloadInterval = config.reloadInterval || 5 * 60 * 1000;
|
||||
var reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", url);
|
||||
@@ -46,7 +46,7 @@ module.exports = NodeHelper.create({
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
console.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new Fetcher(url, reloadInterval, encoding);
|
||||
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
|
||||
fetcher.onReceive(function(fetcher) {
|
||||
self.broadcastFeeds();
|
||||
|
3
modules/default/newsfeed/translations/de.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
|
||||
}
|
3
modules/default/newsfeed/translations/en.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
|
||||
}
|
3
modules/default/newsfeed/translations/es.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"configuration_changed": "Las opciones de configuración para el módulo de suministro de noticias han cambiado. \nVerifique la documentación."
|
||||
}
|
3
modules/default/newsfeed/translations/fr.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
|
||||
}
|
@@ -64,7 +64,12 @@ module.exports = NodeHelper.create({
|
||||
sg.git.fetch().status(function(err, data) {
|
||||
data.module = sg.module;
|
||||
if (!err) {
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
sg.git.log({"-1": null}, function(err, data2) {
|
||||
if (!err && data2.latest && "hash" in data2.latest) {
|
||||
data.hash = data2.latest.hash;
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -34,6 +34,17 @@ Module.register("updatenotification", {
|
||||
}
|
||||
},
|
||||
|
||||
diffLink: function(text) {
|
||||
var localRef = this.status.hash;
|
||||
var remoteRef = this.status.tracking.replace(/.*\//, "");
|
||||
return "<a href=\"https://github.com/MichMich/MagicMirror/compare/"+localRef+"..."+remoteRef+"\" "+
|
||||
"class=\"xsmall dimmed\" "+
|
||||
"style=\"text-decoration: none;\" "+
|
||||
"target=\"_blank\" >" +
|
||||
text +
|
||||
"</a>";
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
@@ -47,20 +58,27 @@ Module.register("updatenotification", {
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
|
||||
var updateInfoKeyName = this.status.behind == 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
var subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: this.status.behind,
|
||||
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);
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
var subtext = document.createElement("div");
|
||||
subtext.innerHTML = this.translate("UPDATE_INFO")
|
||||
.replace("COMMIT_COUNT", this.status.behind + " " + ((this.status.behind == 1) ? "commit" : "commits"))
|
||||
.replace("BRANCH_NAME", this.status.current);
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
}
|
||||
|
110
modules/default/weather/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Weather Module
|
||||
|
||||
This module is aimed to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fullfil both purposes.
|
||||
|
||||
The biggest change is the use of weather providers. This way we are not bound to one API source. And users can choose which API they want to use as their source.
|
||||
|
||||
The module is in a very early stage, and needs a lot of work. It's API isn't set in stone, so keep that in mind when you want to contribute.
|
||||
|
||||
## Example
|
||||
|
||||
 
|
||||
|
||||
## Usage
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
|
||||
````javascript
|
||||
modules: [
|
||||
{
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
// See 'Configuration options' for more information.
|
||||
type: 'current'
|
||||
}
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
## Configuration options
|
||||
|
||||
The following properties can be configured:
|
||||
|
||||
### General options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `weatherProvider` | Which weather provider should be used. <br><br> **Possible values:** `openweathermap` , `darksky` , or `weathergov` <br> **Default value:** `openweathermap`
|
||||
| `type` | Which type of weather data should be displayed. <br><br> **Possible values:** `current` or `forecast` <br> **Default value:** `current`
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit <br> **Default value:** `config.units`
|
||||
| `roundTemp` | Round temperature value to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
| `degreeLabel` | Show the degree label for your chosen units (Metric = C, Imperial = F, Kelvin = K). <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `updateInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `600000` (10 minutes)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `1000` (1 second)
|
||||
| `timeFormat` | Use 12 or 24 hour format. <br><br> **Possible values:** `12` or `24` <br> **Default value:** uses value of _config.timeFormat_
|
||||
| `showPeriod` | Show the period (am/pm) with 12 hour format <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `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:** `.`
|
||||
| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds) <br><br> **Possible values:** `1000` - `5000` <br> **Default value:** `0`
|
||||
| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly interesting when using calender based weather. <br><br> **Default value:** `true`
|
||||
| `calendarClass` | The class for the calender module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
|
||||
#### Current weather options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `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`
|
||||
| `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`
|
||||
| `showWindDirection` | Show the wind direction next to the wind speed. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `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`
|
||||
| `showIndoorHumidity` | If you have another module that emits the `INDOOR_HUMIDITY` notification, the indoor humidity will be displayed <br> **Default value:** `false`
|
||||
| `showFeelsLike` | Shows the Feels like temperature weather. <br><br> **Possible values:** `true` or `false`<br>**Default value:** `true`
|
||||
|
||||
#### Weather forecast options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `tableClass` | The class for the forecast table. <br><br> **Default value:** `'small'`
|
||||
| `colored` | If set to `true`, the min and max temperature are color coded. <br><br> **Default value:** `false`
|
||||
| `showPrecipitationAmount` | Show the amount of rain/snow in the forecast <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
||||
| `maxNumberOfDays` | How many days of forecast to return. Specified by config.js <br><br> **Possible values:** `1` - `16` <br> **Default value:** `5` (5 days) <br> This value is optional. By default the weatherforecast module will return 5 days.
|
||||
|
||||
### Openweathermap options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiVersion` | The OpenWeatherMap API version to use. <br><br> **Default value:** `2.5`
|
||||
| `apiBase` | The OpenWeatherMap base URL. <br><br> **Default value:** `'http://api.openweathermap.org/data/'`
|
||||
| `weatherEndpoint` | The OpenWeatherMap API endPoint. <br><br> **Possible values:** `/weather`, `/forecast` (free users) or `/forecast/daily` (paying users or old apiKey only) <br> **Default value:** `'/weather'`
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `apiKey` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
|
||||
### Darksky options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiBase` | The DarkSky base URL. The darksky api has disabled [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), therefore a proxy is required. <br><br> **Possible value:** `'https://cors-anywhere.herokuapp.com/https://api.darksky.net'` <br> This value is **REQUIRED**
|
||||
| `weatherEndpoint` | The DarkSky API endPoint. <br><br> **Possible values:** `/forecast` <br> This value is **REQUIRED**
|
||||
| `apiKey` | The [DarkSky](https://darksky.net/dev/register) API key, which can be obtained by creating an DarkSky account. <br><br> This value is **REQUIRED**
|
||||
| `lat` | The geo coordinate latitude. <br><br> This value is **REQUIRED**
|
||||
| `lon` | The geo coordinate longitude. <br><br> This value is **REQUIRED**
|
||||
|
||||
### Weather.gov options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiBase` | The weather.gov base URL. <br><br> **Possible value:** `'https://api.weather.gov/points/'` <br> This value is **REQUIRED**
|
||||
| `weatherEndpoint` | The weather.gov API endPoint. <br><br> **Possible values:** `/forecast` for forecast and `/forecast/hourly` for current. <br> This value is **REQUIRED**
|
||||
| `lat` | The geo coordinate latitude. <br><br> This value is **REQUIRED**
|
||||
| `lon` | The geo coordinate longitude. <br><br> This value is **REQUIRED**
|
||||
|
||||
## API Provider Development
|
||||
|
||||
If you want to add another API provider checkout the [Guide](providers).
|
73
modules/default/weather/current.njk
Normal file
@@ -0,0 +1,73 @@
|
||||
{% if current %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
<span>
|
||||
{% if config.useBeaufort %}
|
||||
{{ current.beaufortWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% endif %}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fa fa-long-arrow-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
|
||||
</sup>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity and current.humidity %}
|
||||
<span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidityIcon"></i></sup>
|
||||
{% endif %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
<span>
|
||||
{% if current.nextSunAction() == "sunset" %}
|
||||
{{ current.sunset | formatTime }}
|
||||
{% else %}
|
||||
{{ current.sunrise | formatTime }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="large light">
|
||||
<span class="wi weathericon wi-{{current.weatherType}}"></span>
|
||||
<span class="bright">
|
||||
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="normal light indoor">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<div>
|
||||
<span class="fa fa-home"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<div>
|
||||
<span class="fa fa-tint"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if config.showFeelsLike and not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
|
BIN
modules/default/weather/current.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
32
modules/default/weather/forecast.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{% if forecast %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
{% for f in forecast %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
||||
<td class="align-right bright max-temp">
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
<td class="align-right min-temp">
|
||||
{{ f.minTemperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
BIN
modules/default/weather/forecast.png
Normal file
After Width: | Height: | Size: 16 KiB |
131
modules/default/weather/providers/README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# MagicMirror² Weather Module Weather Provider Development Documentation
|
||||
|
||||
This document describes the way to develop your own MagicMirror² weather module weather provider.
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- The weather provider file: yourprovider.js
|
||||
- [Weather provider methods to implement](#weather-provider-methods-to-implement)
|
||||
- [Weather Provider instance methods](#weather-provider-instance-methods)
|
||||
- [WeatherObject](#weatherobject)
|
||||
|
||||
---
|
||||
|
||||
## The weather provider file: yourprovider.js
|
||||
|
||||
This is the script in which the weather provider will be defined. In it's most simple form, the weather provider must implement the following:
|
||||
|
||||
````javascript
|
||||
WeatherProvider.register("yourprovider", {
|
||||
providerName: "YourProvider",
|
||||
|
||||
fetchCurrentWeather() {},
|
||||
|
||||
fetchWeatherForecast() {}
|
||||
});
|
||||
````
|
||||
|
||||
### Weather provider methods to implement
|
||||
|
||||
#### `fetchCurrentWeather()`
|
||||
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
#### `fetchWeatherForecast()`
|
||||
|
||||
This method is called when the weather module tries to fetch the weather weather of your provider. The implementation of this method is required.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setCurrentWeather(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
### Weather Provider instance methods
|
||||
|
||||
#### `init()`
|
||||
|
||||
Called when a weather provider is initialized.
|
||||
|
||||
#### `setConfig(config)`
|
||||
|
||||
Called to set the config, this config is the same as the weather module's config.
|
||||
|
||||
#### `start()`
|
||||
|
||||
Called when the weather provider is about to start.
|
||||
|
||||
#### `currentWeather()`
|
||||
|
||||
This returns a WeatherDay object for the current weather.
|
||||
|
||||
#### `weatherForecast()`
|
||||
|
||||
This returns an array of WeatherDay objects for the weather forecast.
|
||||
|
||||
#### `fetchedLocation()`
|
||||
|
||||
This returns the name of the fetched location or an empty string.
|
||||
|
||||
#### `setCurrentWeather(currentWeatherObject)`
|
||||
|
||||
Set the currentWeather and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherForecast(weatherForecastArray)`
|
||||
|
||||
Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setFetchedLocation(name)`
|
||||
|
||||
Set the fetched location name.
|
||||
|
||||
#### `updateAvailable()`
|
||||
|
||||
Notify the delegate that new weather is available.
|
||||
|
||||
#### `fetchData(url, method, data)`
|
||||
|
||||
A convenience function to make requests. It returns a promise.
|
||||
|
||||
### WeatherObject
|
||||
|
||||
| Property | Type | Value/Unit |
|
||||
| --- | --- | --- |
|
||||
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric` and `imperial` |
|
||||
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
|
||||
| windSpeed |`number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
|
||||
| windDirection |`number` | Direction of the wind in degrees. |
|
||||
| sunrise |`object` | [Moment.js](https://momentjs.com/) object of sunrise. |
|
||||
| sunset |`object` | [Moment.js](https://momentjs.com/) object of sunset. |
|
||||
| temperature | `number` | Current temperature |
|
||||
| minTemperature | `number` | Lowest temperature of the day. |
|
||||
| maxTemperature | `number` | Highest temperature of the day. |
|
||||
| weatherType | `string` | Icon name of the weather type. <br> Possible values: [WeatherIcons](https://www.npmjs.com/package/weathericons) |
|
||||
| humidity | `number` | Percentage of humidity |
|
||||
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
|
||||
#### Current weather
|
||||
|
||||
For the current weather object the following properties are required:
|
||||
|
||||
- humidity
|
||||
- sunrise
|
||||
- sunset
|
||||
- temperature
|
||||
- units
|
||||
- weatherType
|
||||
- windDirection
|
||||
- windSpeed
|
||||
|
||||
#### Weather forecast
|
||||
|
||||
For the forecast weather object the following properties are required:
|
||||
|
||||
- date
|
||||
- maxTemperature
|
||||
- minTemperature
|
||||
- rain
|
||||
- units
|
||||
- weatherType
|
122
modules/default/weather/providers/darksky.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/* global WeatherProvider, WeatherDay */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: Dark Sky
|
||||
*
|
||||
* By Nicholas Hubbard https://github.com/nhubbard
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Dark Sky.
|
||||
* Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
*/
|
||||
WeatherProvider.register("darksky", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Dark Sky",
|
||||
|
||||
units: {
|
||||
imperial: 'us',
|
||||
metric: 'si'
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if(!data || !data.currently || typeof data.currently.temperature === "undefined") {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
}).catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
});
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if(!data || !data.daily || !data.daily.data.length) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.daily.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
}).catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
});
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
const units = this.units[this.config.units] || "auto";
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=${units}&lang=${this.config.lang}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units);
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.daily.data[0].sunriseTime, "X");
|
||||
currentWeather.sunset = moment(currentWeatherData.daily.data[0].sunsetTime, "X");
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units);
|
||||
|
||||
weather.date = moment(forecast.time, "X");
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
weather.maxTemperature = forecast.temperatureMax;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
weather.snow = 0;
|
||||
|
||||
// The API will return centimeters if units is 'si' and will return inches for 'us'
|
||||
// Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
if (forecast.hasOwnProperty("precipAccumulation")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation;
|
||||
} else if (!isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation * 10;
|
||||
}
|
||||
}
|
||||
|
||||
weather.precipitation = weather.snow;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
||||
"clear-night": "night-clear",
|
||||
"rain": "rain",
|
||||
"snow": "snow",
|
||||
"sleet": "snow",
|
||||
"wind": "wind",
|
||||
"fog": "fog",
|
||||
"cloudy": "cloudy",
|
||||
"partly-cloudy-day": "day-cloudy",
|
||||
"partly-cloudy-night": "night-cloudy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
281
modules/default/weather/providers/openweathermap.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("openweathermap", {
|
||||
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "OpenWeatherMap",
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.main || typeof data.main.temp === "undefined") {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.name}, ${data.sys.country}`);
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.list || !data.list.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units);
|
||||
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||
currentWeather.sunset = moment(currentWeatherData.sys.sunset, "X");
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
|
||||
if (this.config.weatherEndpoint == "/forecast") {
|
||||
return this.fetchForecastHourly(forecasts);
|
||||
} else if (this.config.weatherEndpoint == "/forecast/daily") {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
const days = [new WeatherObject(this.config.units)];
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||
*/
|
||||
fetchForecastHourly(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
let minTemp = [];
|
||||
let maxTemp = [];
|
||||
let rain = 0;
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units);
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
|
||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
rain = 0;
|
||||
snow = 0;
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.dt, "X").format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
|
||||
}
|
||||
|
||||
if (moment(forecast.dt, "X").format("H") >= 8 && moment(forecast.dt, "X").format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"];
|
||||
}
|
||||
}
|
||||
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast (available for paid subscription or old apiKey).
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units);
|
||||
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
weather.maxTemperature = forecast.temp.max;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
weather.rain = 0;
|
||||
weather.snow = 0;
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain / 25.4;
|
||||
} else if (!isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow / 25.4;
|
||||
} else if (!isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
}
|
||||
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy-windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night-clear",
|
||||
"02n": "night-cloudy",
|
||||
"03n": "night-cloudy",
|
||||
"04n": "night-cloudy",
|
||||
"09n": "night-showers",
|
||||
"10n": "night-rain",
|
||||
"11n": "night-thunderstorm",
|
||||
"13n": "night-snow",
|
||||
"50n": "night-alt-cloudy-windy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if(this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if(this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
} else {
|
||||
this.hide(this.config.animationSpeed, {lockString:this.identifier});
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.apiKey;
|
||||
|
||||
return params;
|
||||
}
|
||||
});
|
256
modules/default/weather/providers/weathergov.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: weather.gov
|
||||
*
|
||||
* By Vince Peri
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for weather.gov.
|
||||
* Note that this is only for US locations (lat and lon) and does not require an API key
|
||||
* Since it is free, there are some items missing - like sunrise, sunset, humidity, etc.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weathergov", {
|
||||
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
},
|
||||
|
||||
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units);
|
||||
|
||||
currentWeather.temperature = currentWeatherData.temperature;
|
||||
currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1);
|
||||
currentWeather.windDirection = this.convertDirectiontoDegrees(currentWeatherData.windDirection);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast.
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
let minTemp = [];
|
||||
let maxTemp = [];
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units);
|
||||
weather.precipitation = 0;
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
|
||||
if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) {
|
||||
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
weather.precipitation = 0;
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.startTime).format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment(forecast.startTime);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
if (moment(forecast.startTime).format("H") >= 8 && moment(forecast.startTime).format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
minTemp.push(forecast.temperature);
|
||||
maxTemp.push(forecast.temperature);
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType, isDaytime) {
|
||||
//https://w1.weather.gov/xml/current_obs/weather.php
|
||||
// There are way too many types to create, so lets just look for certain strings
|
||||
|
||||
if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) {
|
||||
if (isDaytime) {
|
||||
return "day-cloudy";
|
||||
}
|
||||
|
||||
return "night-cloudy";
|
||||
} else if (weatherType.includes("Overcast")) {
|
||||
if (isDaytime) {
|
||||
return "cloudy";
|
||||
}
|
||||
|
||||
return "night-cloudy";
|
||||
} else if (weatherType.includes("Freezing") || weatherType.includes("Ice")) {
|
||||
return "rain-mix";
|
||||
} else if (weatherType.includes("Snow")) {
|
||||
if (isDaytime) {
|
||||
return "snow";
|
||||
}
|
||||
|
||||
return "night-snow";
|
||||
} else if (weatherType.includes("Thunderstorm")) {
|
||||
if (isDaytime) {
|
||||
return "thunderstorm";
|
||||
}
|
||||
|
||||
return "night-thunderstorm";
|
||||
} else if (weatherType.includes("Showers")) {
|
||||
if (isDaytime) {
|
||||
return "showers";
|
||||
}
|
||||
|
||||
return "night-showers";
|
||||
} else if (weatherType.includes("Rain") || weatherType.includes("Drizzle")) {
|
||||
if (isDaytime) {
|
||||
return "rain";
|
||||
}
|
||||
|
||||
return "night-rain";
|
||||
} else if (weatherType.includes("Breezy") || weatherType.includes("Windy")) {
|
||||
if (isDaytime) {
|
||||
return "cloudy-windy";
|
||||
}
|
||||
|
||||
return "night-alt-cloudy-windy";
|
||||
} else if (weatherType.includes("Fair") || weatherType.includes("Clear") || weatherType.includes("Few") || weatherType.includes("Sunny")) {
|
||||
if (isDaytime) {
|
||||
return "day-sunny";
|
||||
}
|
||||
|
||||
return "night-clear";
|
||||
} else if (weatherType.includes("Dust") || weatherType.includes("Sand")) {
|
||||
return "dust";
|
||||
} else if (weatherType.includes("Fog")) {
|
||||
return "fog";
|
||||
} else if (weatherType.includes("Smoke")) {
|
||||
return "smoke";
|
||||
} else if (weatherType.includes("Haze")) {
|
||||
return "day-haze";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/*
|
||||
Convert the direction into Degrees
|
||||
*/
|
||||
convertDirectiontoDegrees(direction) {
|
||||
if (direction === "NNE"){
|
||||
return 33.75;
|
||||
} else if (direction === "NE") {
|
||||
return 56.25;
|
||||
} else if (direction === "ENE") {
|
||||
return 78.75;
|
||||
} else if (direction === "E") {
|
||||
return 101.25;
|
||||
} else if (direction === "ESE") {
|
||||
return 123.75;
|
||||
} else if (direction === "SE") {
|
||||
return 146.25;
|
||||
} else if (direction === "SSE") {
|
||||
return 168.75;
|
||||
} else if (direction === "S") {
|
||||
return 191.25;
|
||||
} else if (direction === "SSW") {
|
||||
return 213.75;
|
||||
} else if (direction === "SW") {
|
||||
return 236.25;
|
||||
} else if (direction === "WSW") {
|
||||
return 258.75;
|
||||
} else if (direction === "W") {
|
||||
return 281.25;
|
||||
} else if (direction === "WNW") {
|
||||
return 303.75;
|
||||
} else if (direction === "NW") {
|
||||
return 326.25;
|
||||
} else if (direction === "NNW") {
|
||||
return 348.75;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
});
|
49
modules/default/weather/weather.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.weather .weathericon,
|
||||
.weather .fa-home {
|
||||
font-size: 75%;
|
||||
line-height: 65px;
|
||||
display: inline-block;
|
||||
-ms-transform: translate(0, -3px); /* IE 9 */
|
||||
-webkit-transform: translate(0, -3px); /* Safari */
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.weather .humidityIcon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.weather .humidity-padding {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.weather .day {
|
||||
padding-left: 0;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.weather .weather-icon {
|
||||
padding-right: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weather .min-temp {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather .precipitation {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather tr .weathericon {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.weather tr.colored .min-temp {
|
||||
color: #bcddff;
|
||||
}
|
||||
|
||||
.weather tr.colored .max-temp {
|
||||
color: #ff8e99;
|
||||
}
|
245
modules/default/weather/weather.js
Normal file
@@ -0,0 +1,245 @@
|
||||
/* global Module, WeatherProvider */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
Module.register("weather",{
|
||||
// Default module config.
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000,
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", //current, forecast
|
||||
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
showWindDirection: true,
|
||||
showWindDirectionAsArrow: false,
|
||||
useBeaufort: true,
|
||||
lang: config.language,
|
||||
showHumidity: false,
|
||||
degreeLabel: false,
|
||||
decimalSymbol: ".",
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "http://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "/weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
showFeelsLike: true
|
||||
},
|
||||
|
||||
// Module properties.
|
||||
weatherProvider: null,
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function() {
|
||||
return ["font-awesome.css", "weather-icons.css", "weather.css"];
|
||||
},
|
||||
|
||||
// Return the scripts that are necessary for the weather module.
|
||||
getScripts: function () {
|
||||
return [
|
||||
"moment.js",
|
||||
"weatherprovider.js",
|
||||
"weatherobject.js",
|
||||
this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")
|
||||
];
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function() {
|
||||
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
|
||||
return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
// Start the weather module.
|
||||
start: function () {
|
||||
moment.locale(this.config.lang);
|
||||
// Initialize the weather provider.
|
||||
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
|
||||
|
||||
// Let the weather provider know we are starting.
|
||||
this.weatherProvider.start();
|
||||
|
||||
// Add custom filters
|
||||
this.addFilters();
|
||||
|
||||
// Schedule the first update.
|
||||
this.scheduleUpdate(this.config.initialLoadDelay);
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function(notification, payload, sender) {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
var senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
this.firstEvent = false;
|
||||
|
||||
for (var e in payload) {
|
||||
var event = payload[e];
|
||||
if (event.location || event.geo) {
|
||||
this.firstEvent = event;
|
||||
//Log.log("First upcoming event with location: ", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (notification === "INDOOR_TEMPERATURE") {
|
||||
this.indoorTemperature = this.roundValue(payload);
|
||||
this.updateDom(300);
|
||||
} else if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(300);
|
||||
}
|
||||
},
|
||||
|
||||
// Select the template depending on the display type.
|
||||
getTemplate: function () {
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData: function () {
|
||||
return {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// What to do when the weather provider has new information available?
|
||||
updateAvailable: function() {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
this.scheduleUpdate();
|
||||
},
|
||||
|
||||
scheduleUpdate: function(delay = null) {
|
||||
var nextLoad = this.config.updateInterval;
|
||||
if (delay !== null && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.config.type === "forecast") {
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
} else {
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
}
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
roundValue: function(temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
this.nunjucksEnvironment().addFilter("formatTime", function(date) {
|
||||
date = moment(date);
|
||||
|
||||
if (this.config.timeFormat !== 24) {
|
||||
if (this.config.showPeriod) {
|
||||
if (this.config.showPeriodUpper) {
|
||||
return date.format("h:mm A");
|
||||
} else {
|
||||
return date.format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
return date.format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
return date.format("HH:mm");
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("unit", function (value, type) {
|
||||
if (type === "temperature") {
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
value += "°";
|
||||
}
|
||||
if (this.config.degreeLabel) {
|
||||
if (this.config.units === "metric") {
|
||||
value += "C";
|
||||
} else if (this.config.units === "imperial") {
|
||||
value += "F";
|
||||
} else {
|
||||
value += "K";
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
} else {
|
||||
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
|
||||
}
|
||||
} else if (type === "humidity") {
|
||||
value += "%"
|
||||
}
|
||||
|
||||
return value;
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("roundValue", function(value) {
|
||||
return this.roundValue(value);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("decimalSymbol", function(value) {
|
||||
return value.toString().replace(/\./g, this.config.decimalSymbol);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("calcNumSteps", function(forecast) {
|
||||
return Math.min(forecast.length, this.config.maxNumberOfDays);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("opacity", function(currentStep, numSteps) {
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startingPoint = numSteps * this.config.fadePoint;
|
||||
var numFadesteps = numSteps - startingPoint;
|
||||
if (currentStep >= startingPoint) {
|
||||
return 1 - (currentStep - startingPoint) / numFadesteps;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
102
modules/default/weather/weatherobject.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/* global Class */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a day which includes weather information.
|
||||
*/
|
||||
|
||||
// Currently this is focused on the information which is necessary for the current weather.
|
||||
// As soon as we start implementing the forecast, mode properties will be added.
|
||||
|
||||
class WeatherObject {
|
||||
constructor(units) {
|
||||
this.units = units;
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
this.sunrise = null;
|
||||
this.sunset = null;
|
||||
this.temperature = null;
|
||||
this.minTemperature = null;
|
||||
this.maxTemperature = null;
|
||||
this.weatherType = null;
|
||||
this.humidity = null;
|
||||
this.rain = null;
|
||||
this.snow = null;
|
||||
this.precipitation = null;
|
||||
}
|
||||
|
||||
cardinalWindDirection() {
|
||||
if (this.windDirection > 11.25 && this.windDirection <= 33.75){
|
||||
return "NNE";
|
||||
} else if (this.windDirection > 33.75 && this.windDirection <= 56.25) {
|
||||
return "NE";
|
||||
} else if (this.windDirection > 56.25 && this.windDirection <= 78.75) {
|
||||
return "ENE";
|
||||
} else if (this.windDirection > 78.75 && this.windDirection <= 101.25) {
|
||||
return "E";
|
||||
} else if (this.windDirection > 101.25 && this.windDirection <= 123.75) {
|
||||
return "ESE";
|
||||
} else if (this.windDirection > 123.75 && this.windDirection <= 146.25) {
|
||||
return "SE";
|
||||
} else if (this.windDirection > 146.25 && this.windDirection <= 168.75) {
|
||||
return "SSE";
|
||||
} else if (this.windDirection > 168.75 && this.windDirection <= 191.25) {
|
||||
return "S";
|
||||
} else if (this.windDirection > 191.25 && this.windDirection <= 213.75) {
|
||||
return "SSW";
|
||||
} else if (this.windDirection > 213.75 && this.windDirection <= 236.25) {
|
||||
return "SW";
|
||||
} else if (this.windDirection > 236.25 && this.windDirection <= 258.75) {
|
||||
return "WSW";
|
||||
} else if (this.windDirection > 258.75 && this.windDirection <= 281.25) {
|
||||
return "W";
|
||||
} else if (this.windDirection > 281.25 && this.windDirection <= 303.75) {
|
||||
return "WNW";
|
||||
} else if (this.windDirection > 303.75 && this.windDirection <= 326.25) {
|
||||
return "NW";
|
||||
} else if (this.windDirection > 326.25 && this.windDirection <= 348.75) {
|
||||
return "NNW";
|
||||
} else {
|
||||
return "N";
|
||||
}
|
||||
}
|
||||
|
||||
beaufortWindSpeed() {
|
||||
const windInKmh = this.units === "imperial" ? this.windSpeed * 1.609344 : this.windSpeed * 60 * 60 / 1000;
|
||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (const [index, speed] of speeds.entries()) {
|
||||
if (speed > windInKmh) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
||||
feelsLike() {
|
||||
const windInMph = this.units === "imperial" ? this.windSpeed : this.windSpeed * 2.23694;
|
||||
const tempInF = this.units === "imperial" ? this.temperature : this.temperature * 9 / 5 + 32;
|
||||
let feelsLike = tempInF;
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && this.humidity > 40) {
|
||||
feelsLike = -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;
|
||||
}
|
||||
|
||||
return this.units === "imperial" ? feelsLike : (feelsLike - 32) * 5 / 9;
|
||||
}
|
||||
}
|
154
modules/default/weather/weatherprovider.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/* global Class */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Base BluePrint for the WeatherProvider
|
||||
*/
|
||||
var WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
|
||||
// The following properties have accestor methods.
|
||||
// Try to not access them directly.
|
||||
currentWeatherObject: null,
|
||||
weatherForecastArray: null,
|
||||
fetchedLocationName: null,
|
||||
|
||||
// The following properties will be set automaticly.
|
||||
// You do not need to overwrite these properties.
|
||||
config: null,
|
||||
delegate: null,
|
||||
providerIdentifier: null,
|
||||
|
||||
|
||||
// Weather Provider Methods
|
||||
// All the following methods can be overwrited, although most are good as they are.
|
||||
|
||||
// Called when a weather provider is initialized.
|
||||
init: function(config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} initialized.`);
|
||||
},
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function(config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} config set.`, this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function() {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the current weather.
|
||||
// This method should definetly be overwritten in the provider.
|
||||
fetchCurrentWeather: function() {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchCurrentWeather method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather forecast.
|
||||
// This method should definetly be overwritten in the provider.
|
||||
fetchWeatherForecast: function() {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
currentWeather: function() {
|
||||
return this.currentWeatherObject;
|
||||
},
|
||||
|
||||
// This returns an array of WeatherDay objects for the weather forecast.
|
||||
weatherForecast: function() {
|
||||
return this.weatherForecastArray;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function() {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Set the currentWeather and notify the delegate that new information is available.
|
||||
setCurrentWeather: function(currentWeatherObject) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.currentWeatherObject = currentWeatherObject;
|
||||
|
||||
this.updateAvailable();
|
||||
},
|
||||
|
||||
// Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
setWeatherForecast: function(weatherForecastArray) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
|
||||
this.updateAvailable();
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function(name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Notify the delegate that new weather is available.
|
||||
updateAvailable: function() {
|
||||
this.delegate.updateAvailable(this);
|
||||
},
|
||||
|
||||
// A convinience function to make requests. It returns a promise.
|
||||
fetchData: function(url, method = "GET", data = null) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
resolve(JSON.parse(this.response));
|
||||
} else {
|
||||
reject(request)
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Collection of registered weather providers.
|
||||
*/
|
||||
WeatherProvider.providers = [];
|
||||
|
||||
/**
|
||||
* Static method to register a new weather provider.
|
||||
*/
|
||||
WeatherProvider.register = function(providerIdentifier, providerDetails) {
|
||||
WeatherProvider.providers[providerIdentifier.toLowerCase()] = WeatherProvider.extend(providerDetails);
|
||||
};
|
||||
|
||||
/**
|
||||
* Static method to initialize a new weather provider.
|
||||
*/
|
||||
WeatherProvider.initialize = function(providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
|
||||
var provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
|
||||
provider.delegate = delegate;
|
||||
provider.setConfig(delegate.config);
|
||||
|
||||
provider.providerIdentifier = providerIdentifier;
|
||||
if (!provider.providerName) {
|
||||
provider.providerName = providerIdentifier;
|
||||
}
|
||||
|
||||
return provider;
|
||||
};
|
@@ -2,6 +2,11 @@
|
||||
The `weatherforecast` module is one of the default modules of the MagicMirror.
|
||||
This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature.
|
||||
|
||||
## Screenshots
|
||||
|
||||
- 5 day forecast
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -28,15 +33,16 @@ The following properties can be configured:
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](http://openweathermap.org/help/city_list.txt) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `appid` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` =Fahrenheit <br> **Default value:** `config.units`
|
||||
| `roundTemp` | Round temperature values to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
| `maxNumberOfDays` | How many days of forecast to return. Specified by config.js <br><br> **Possible values:** `1` - `16` <br> **Default value:** `7` (7 days) <br> This value is optional. By default the weatherforecast module will return 7 days.
|
||||
| `showRainAmount` | Should the predicted rain amount be displayed? <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false` <br> This value is optional. By default the weatherforecast module will not display the predicted amount of rain.
|
||||
| `updateInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `600000` (10 minutes)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `1000` (1 second)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `1000` (1 second)
|
||||
| `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:** `.`
|
||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
||||
| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds) <br><br> **Possible values:** `1000` - `5000` <br> **Default value:** `2500` (2.5 seconds delay. This delay is used to keep the OpenWeather API happy.)
|
||||
@@ -45,9 +51,11 @@ The following properties can be configured:
|
||||
| `apiBase` | The OpenWeatherMap base URL. <br><br> **Default value:** `'http://api.openweathermap.org/data/'`
|
||||
| `forecastEndpoint` | The OpenWeatherMap API endPoint. <br><br> **Default value:** `'forecast/daily'`
|
||||
| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather. <br><br> **Default value:** `true`
|
||||
| `calendarClass` | The class for the calender module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
| `calendarClass` | The class for the calendar module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
| `tableClass` | Name of the classes issued from `main.css`. <br><br> **Possible values:** xsmall, small, medium, large, xlarge. <br> **Default value:** _small._
|
||||
| `iconTable` | The conversion table to convert the weather conditions to weather-icons. <br><br> **Default value:** view table below
|
||||
`colored` | If set 'colored' to true the min-temp get a blue tone and the max-temp get a red tone. <br><br> **Default value:** `'false'`
|
||||
| `colored` | If set `colored` to `true` the min-temp gets a blue tone and the max-temp gets a red tone. <br><br> **Default value:** `'false'`
|
||||
| `scale ` | If set to `true` the module will display `C` for Celsius degrees and `F` for Fahrenheit degrees after the number, based on the value of the `units` option, otherwise only the ° character is displayed. <br><br> **Default value:** `false`
|
||||
|
||||
#### Default Icon Table
|
||||
````javascript
|
||||
|
BIN
modules/default/weatherforecast/forecast_screenshot.png
Normal file
After Width: | Height: | Size: 84 KiB |
@@ -21,6 +21,7 @@ Module.register("weatherforecast",{
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
lang: config.language,
|
||||
decimalSymbol: ".",
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
colored: false,
|
||||
@@ -30,11 +31,12 @@ Module.register("weatherforecast",{
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "http://api.openweathermap.org/data/",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
forecastEndpoint: "forecast/daily",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
|
||||
roundTemp: false,
|
||||
|
||||
@@ -116,7 +118,7 @@ Module.register("weatherforecast",{
|
||||
}
|
||||
|
||||
var table = document.createElement("table");
|
||||
table.className = "small";
|
||||
table.className = this.config.tableClass;
|
||||
|
||||
for (var f in this.forecast) {
|
||||
var forecast = this.forecast[f];
|
||||
@@ -141,13 +143,16 @@ Module.register("weatherforecast",{
|
||||
iconCell.appendChild(icon);
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if(this.config.scale) {
|
||||
switch(this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel = " °C";
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel = " °F";
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel = "K";
|
||||
@@ -155,13 +160,17 @@ Module.register("weatherforecast",{
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.decimalSymbol === "" || this.config.decimalSymbol === " ") {
|
||||
this.config.decimalSymbol = ".";
|
||||
}
|
||||
|
||||
var maxTempCell = document.createElement("td");
|
||||
maxTempCell.innerHTML = forecast.maxTemp + degreeLabel;
|
||||
maxTempCell.innerHTML = forecast.maxTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
maxTempCell.className = "align-right bright max-temp";
|
||||
row.appendChild(maxTempCell);
|
||||
|
||||
var minTempCell = document.createElement("td");
|
||||
minTempCell.innerHTML = forecast.minTemp + degreeLabel;
|
||||
minTempCell.innerHTML = forecast.minTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
minTempCell.className = "align-right min-temp";
|
||||
row.appendChild(minTempCell);
|
||||
|
||||
@@ -171,7 +180,7 @@ Module.register("weatherforecast",{
|
||||
rainCell.innerHTML = "";
|
||||
} else {
|
||||
if(config.units !== "imperial") {
|
||||
rainCell.innerHTML = forecast.rain + " mm";
|
||||
rainCell.innerHTML = parseFloat(forecast.rain).toFixed(1) + " mm";
|
||||
} else {
|
||||
rainCell.innerHTML = (parseFloat(forecast.rain) / 25.4).toFixed(2) + " in";
|
||||
}
|
||||
@@ -254,7 +263,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.");
|
||||
}
|
||||
|
||||
@@ -293,12 +301,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;
|
||||
@@ -335,8 +337,15 @@ Module.register("weatherforecast",{
|
||||
var forecast = data.list[i];
|
||||
this.parserDataWeather(forecast); // hack issue #1017
|
||||
|
||||
var day = moment(forecast.dt, "X").format("ddd");
|
||||
var hour = moment(forecast.dt, "X").format("H");
|
||||
var day;
|
||||
var hour;
|
||||
if(!!forecast.dt_txt) {
|
||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
|
||||
} else {
|
||||
day = moment(forecast.dt, "X").format("ddd");
|
||||
hour = moment(forecast.dt, "X").format("H");
|
||||
}
|
||||
|
||||
if (day !== lastDay) {
|
||||
var forecastData = {
|
||||
@@ -344,11 +353,16 @@ Module.register("weatherforecast",{
|
||||
icon: this.config.iconTable[forecast.weather[0].icon],
|
||||
maxTemp: this.roundValue(forecast.temp.max),
|
||||
minTemp: this.roundValue(forecast.temp.min),
|
||||
rain: this.roundValue(forecast.rain)
|
||||
rain: forecast.rain
|
||||
};
|
||||
|
||||
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;
|
||||
|
10
modules/node_modules/node_helper/index.js
generated
vendored
@@ -23,6 +23,16 @@ NodeHelper = Class.extend({
|
||||
console.log("Starting module helper: " + this.name);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
* Called when the MagicMirror server receives a `SIGINT`
|
||||
* Close any open connections, stop any sub-processes and
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop: function() {
|
||||
console.log("Stopping module helper: " + this.name);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
* This method is called when a socket notification arrives.
|
||||
*
|
||||
|
7618
package-lock.json
generated
Normal file
43
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.1.3",
|
||||
"version": "2.8.0-develop",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,8 @@
|
||||
"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",
|
||||
@@ -33,39 +34,43 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"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",
|
||||
"grunt-markdownlint": "^1.0.39",
|
||||
"grunt-markdownlint": "^1.0.43",
|
||||
"grunt-stylelint": "latest",
|
||||
"grunt-yamllint": "latest",
|
||||
"http-auth": "^3.1.3",
|
||||
"jshint": "^2.9.4",
|
||||
"mocha": "^3.4.2",
|
||||
"http-auth": "^3.2.3",
|
||||
"jsdom": "^11.6.2",
|
||||
"jshint": "^2.9.5",
|
||||
"mocha": "^4.1.0",
|
||||
"mocha-each": "^1.1.0",
|
||||
"spectron": "3.6.x",
|
||||
"stylelint": "^8.0.0",
|
||||
"spectron": "^3.8.0",
|
||||
"stylelint": "^8.4.0",
|
||||
"stylelint-config-standard": "latest",
|
||||
"time-grunt": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.17.2",
|
||||
"ajv": "6.5.5",
|
||||
"body-parser": "^1.18.2",
|
||||
"colors": "^1.1.2",
|
||||
"electron": "^1.6.10",
|
||||
"express": "^4.15.3",
|
||||
"electron": "^3.0.13",
|
||||
"express": "^4.16.2",
|
||||
"express-ipfilter": "0.3.1",
|
||||
"feedme": "latest",
|
||||
"helmet": "^3.6.1",
|
||||
"helmet": "^3.9.0",
|
||||
"home-path": "^1.0.6",
|
||||
"iconv-lite": "latest",
|
||||
"mocha-logger": "^1.0.5",
|
||||
"mocha-logger": "^1.0.6",
|
||||
"moment": "latest",
|
||||
"request": "^2.81.0",
|
||||
"rrule-alt": "^2.2.5",
|
||||
"simple-git": "^1.73.0",
|
||||
"socket.io": "^2.0.2",
|
||||
"request": "^2.87.0",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^1.85.0",
|
||||
"socket.io": "^2.1.1",
|
||||
"valid-url": "latest",
|
||||
"walk": "latest"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
if [ -z "$DISPLAY" ]; then #If not set DISPLAY is SSH remote or tty
|
||||
export DISPLAY=:0 # Set by defaul display
|
||||
export DISPLAY=:0 # Set by default display
|
||||
fi
|
||||
electron js/electron.js $1
|
||||
|
@@ -1,6 +1,5 @@
|
||||
var app = require("../js/app.js");
|
||||
app.start(function(config) {
|
||||
console.log("");
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
console.log("Ready to go! Please point your browser to: http://" + bindAddress + ":" + config.port);
|
||||
console.log("\nReady to go! Please point your browser to: http://" + bindAddress + ":" + config.port);
|
||||
});
|
||||
|
@@ -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 don't containt syntax error :)");
|
||||
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();
|
||||
};
|
13
tests/configs/data/StripComments.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// Escaped
|
||||
"FOO\"BAR": "Today",
|
||||
|
||||
/*
|
||||
* The following lines
|
||||
* represent cardinal directions
|
||||
*/
|
||||
"N": "N",
|
||||
"E": "E",
|
||||
"S": "S",
|
||||
"W": "W"
|
||||
}
|
33
tests/configs/data/TranslationTest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"LOADING": "Loading …",
|
||||
|
||||
"TODAY": "Today",
|
||||
"TOMORROW": "Tomorrow",
|
||||
"DAYAFTERTOMORROW": "In 2 days",
|
||||
"RUNNING": "Ends in",
|
||||
"EMPTY": "No upcoming events.",
|
||||
|
||||
"WEEK": "Week {weekNumber}",
|
||||
|
||||
"N": "N",
|
||||
"NNE": "NNE",
|
||||
"NE": "NE",
|
||||
"ENE": "ENE",
|
||||
"E": "E",
|
||||
"ESE": "ESE",
|
||||
"SE": "SE",
|
||||
"SSE": "SSE",
|
||||
"S": "S",
|
||||
"SSW": "SSW",
|
||||
"SW": "SW",
|
||||
"WSW": "WSW",
|
||||
"W": "W",
|
||||
"WNW": "WNW",
|
||||
"NW": "NW",
|
||||
"NNW": "NNW",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
||||
"UPDATE_INFO_SINGLE": "The current installation is COMMIT_COUNT commit behind on the BRANCH_NAME branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "The current installation is COMMIT_COUNT commits behind on the BRANCH_NAME branch."
|
||||
}
|
33
tests/configs/data/en.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"LOADING": "Loading …",
|
||||
|
||||
"TODAY": "Today",
|
||||
"TOMORROW": "Tomorrow",
|
||||
"DAYAFTERTOMORROW": "In 2 days",
|
||||
"RUNNING": "Ends in",
|
||||
"EMPTY": "No upcoming events.",
|
||||
|
||||
"WEEK": "Week {weekNumber}",
|
||||
|
||||
"N": "N",
|
||||
"NNE": "NNE",
|
||||
"NE": "NE",
|
||||
"ENE": "ENE",
|
||||
"E": "E",
|
||||
"ESE": "ESE",
|
||||
"SE": "SE",
|
||||
"SSE": "SSE",
|
||||
"S": "S",
|
||||
"SSW": "SSW",
|
||||
"SW": "SW",
|
||||
"WSW": "WSW",
|
||||
"W": "W",
|
||||
"WNW": "WNW",
|
||||
"NW": "NW",
|
||||
"NNW": "NNW",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
||||
"UPDATE_INFO_SINGLE": "The current installation is COMMIT_COUNT commit behind on the BRANCH_NAME branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "The current installation is COMMIT_COUNT commits behind on the BRANCH_NAME branch."
|
||||
}
|
@@ -36,7 +36,7 @@ describe("Electron app environment", function() {
|
||||
it("should open a browserwindow", function() {
|
||||
return app.client
|
||||
.waitUntilWindowLoaded()
|
||||
.browserWindow.focus()
|
||||
// .browserWindow.focus()
|
||||
.getWindowCount()
|
||||
.should.eventually.equal(1)
|
||||
.browserWindow.isMinimized()
|
||||
|
129
tests/e2e/translations_spec.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const chai = require("chai");
|
||||
const expect = chai.expect;
|
||||
const mlog = require("mocha-logger");
|
||||
const translations = require("../../translations/translations.js");
|
||||
const helmet = require("helmet");
|
||||
const {JSDOM} = require("jsdom");
|
||||
const express = require("express");
|
||||
|
||||
describe("Translations", function() {
|
||||
let server;
|
||||
|
||||
before(function() {
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(function (req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
next();
|
||||
});
|
||||
app.use("/translations", express.static(path.join(__dirname, "..", "..", "translations")));
|
||||
|
||||
server = app.listen(3000);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it("should have a translation file in the specified path", function() {
|
||||
for(let language in translations) {
|
||||
const file = fs.statSync(translations[language]);
|
||||
expect(file.isFile()).to.be.equal(true);
|
||||
}
|
||||
});
|
||||
|
||||
const mmm = {
|
||||
name: "TranslationTest",
|
||||
file(file) {
|
||||
return `http://localhost:3000/${file}`;
|
||||
}
|
||||
};
|
||||
|
||||
describe("Parsing language files through the Translator class", function() {
|
||||
for(let language in translations) {
|
||||
it(`should parse ${language}`, function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
|
||||
Translator.load(mmm, translations[language], false, function() {
|
||||
expect(Translator.translations[mmm.name]).to.be.an("object");
|
||||
expect(Object.keys(Translator.translations[mmm.name]).length).to.be.at.least(1);
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Same keys", function() {
|
||||
let base;
|
||||
|
||||
before(function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
|
||||
Translator.load(mmm, translations.en, false, function() {
|
||||
base = Object.keys(Translator.translations[mmm.name]).sort();
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
for (let language in translations) {
|
||||
if (language === "en") {
|
||||
continue;
|
||||
}
|
||||
|
||||
describe(`Translation keys of ${language}`, function() {
|
||||
let keys;
|
||||
|
||||
before(function(done){
|
||||
const dom = new JSDOM(`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
|
||||
Translator.load(mmm, translations[language], false, function() {
|
||||
keys = Object.keys(Translator.translations[mmm.name]).sort();
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it(`${language} keys should be in base`, function() {
|
||||
keys.forEach(function(key) {
|
||||
expect(base.indexOf(key)).to.be.at.least(0);
|
||||
});
|
||||
});
|
||||
|
||||
it(`${language} should contain all base keys`, function() {
|
||||
// TODO: when all translations are fixed, use
|
||||
// expect(keys).to.deep.equal(base);
|
||||
// instead of the try-catch-block
|
||||
|
||||
try {
|
||||
expect(keys).to.deep.equal(base);
|
||||
} catch(e) {
|
||||
if (e instanceof chai.AssertionError) {
|
||||
const diff = base.filter(key => !keys.includes(key));
|
||||
mlog.pending(`Missing Translations for language ${language}: ${diff}`);
|
||||
this.skip();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@@ -25,7 +25,7 @@ describe("Check configuration without modules", function () {
|
||||
});
|
||||
|
||||
before(function () {
|
||||
// Set config sample for use in test
|
||||
// Set config sample for use in test
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/without_modules.js";
|
||||
});
|
||||
|
||||
|
109
tests/unit/classes/class_spec.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const chai = require("chai");
|
||||
const expect = chai.expect;
|
||||
const path = require("path");
|
||||
const {JSDOM} = require("jsdom");
|
||||
|
||||
describe("File js/class", function() {
|
||||
describe("Test function cloneObject", function() {
|
||||
let clone;
|
||||
let dom;
|
||||
|
||||
before(function(done) {
|
||||
dom = new JSDOM(`<script>var Log = {log: function() {}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {cloneObject} = dom.window;
|
||||
clone = cloneObject;
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should clone object", function() {
|
||||
const expected = {name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror"};
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.deep.equal(expected);
|
||||
expect(expected === obj).to.equal(false);
|
||||
});
|
||||
|
||||
it("should clone array", function() {
|
||||
const expected = [1, null, undefined, "TEST"];
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.deep.equal(expected);
|
||||
expect(expected === obj).to.equal(false);
|
||||
});
|
||||
|
||||
it("should clone number", function() {
|
||||
let expected = 1;
|
||||
let obj = clone(expected);
|
||||
expect(obj).to.equal(expected);
|
||||
|
||||
expected = 1.23;
|
||||
obj = clone(expected);
|
||||
expect(obj).to.equal(expected);
|
||||
});
|
||||
|
||||
it("should clone string", function() {
|
||||
const expected = "Perfect stranger";
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.equal(expected);
|
||||
});
|
||||
|
||||
it("should clone undefined", function() {
|
||||
const expected = undefined;
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.equal(expected);
|
||||
});
|
||||
|
||||
it("should clone null", function() {
|
||||
const expected = null;
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.equal(expected);
|
||||
});
|
||||
|
||||
it("should clone nested object", function() {
|
||||
const expected = {
|
||||
name: "fewieden",
|
||||
link: "https://github.com/fewieden",
|
||||
versions: ["2.0", "2.1", "2.2"],
|
||||
answerForAllQuestions: 42,
|
||||
properties: {
|
||||
items: [{foo: "bar"}, {lorem: "ipsum"}],
|
||||
invalid: undefined,
|
||||
nothing: null
|
||||
}
|
||||
};
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.deep.equal(expected);
|
||||
expect(expected === obj).to.equal(false);
|
||||
expect(expected.versions === obj.versions).to.equal(false);
|
||||
expect(expected.properties === obj.properties).to.equal(false);
|
||||
expect(expected.properties.items === obj.properties.items).to.equal(false);
|
||||
expect(expected.properties.items[0] === obj.properties.items[0]).to.equal(false);
|
||||
expect(expected.properties.items[1] === obj.properties.items[1]).to.equal(false);
|
||||
});
|
||||
|
||||
describe("Test lockstring code", function() {
|
||||
let log;
|
||||
|
||||
before(function() {
|
||||
log = dom.window.Log.log;
|
||||
dom.window.Log.log = function cmp(str) {
|
||||
expect(str).to.equal("lockStrings");
|
||||
};
|
||||
});
|
||||
|
||||
after(function() {
|
||||
dom.window.Log.log = log;
|
||||
});
|
||||
|
||||
it("should clone object and log lockStrings", function() {
|
||||
const expected = {name: "Module", lockStrings: "stringLock"};
|
||||
const obj = clone(expected);
|
||||
expect(obj).to.deep.equal(expected);
|
||||
expect(expected === obj).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
17
tests/unit/classes/deprecated_spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const chai = require("chai");
|
||||
const expect = chai.expect;
|
||||
const deprecated = require("../../../js/deprecated");
|
||||
|
||||
describe("Deprecated", function() {
|
||||
it("should be an object", function() {
|
||||
expect(deprecated).to.be.an("object");
|
||||
});
|
||||
|
||||
it("should contain configs array with deprecated options as strings", function() {
|
||||
expect(deprecated.configs).to.be.an("array");
|
||||
for (let option of deprecated.configs) {
|
||||
expect(option).to.be.an("string");
|
||||
}
|
||||
expect(deprecated.configs).to.include("kioskmode");
|
||||
});
|
||||
});
|
298
tests/unit/classes/translator_spec.js
Normal file
@@ -0,0 +1,298 @@
|
||||
const chai = require("chai");
|
||||
const expect = chai.expect;
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
const {JSDOM} = require("jsdom");
|
||||
const express = require("express");
|
||||
|
||||
describe("Translator", function() {
|
||||
let server;
|
||||
|
||||
before(function() {
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(function (req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
next();
|
||||
});
|
||||
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "configs", "data")));
|
||||
|
||||
server = app.listen(3000);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
server.close();
|
||||
});
|
||||
|
||||
describe("translate", function() {
|
||||
const translations = {
|
||||
"MMM-Module": {
|
||||
"Hello": "Hallo",
|
||||
"Hello {username}": "Hallo {username}"
|
||||
}
|
||||
};
|
||||
|
||||
const coreTranslations = {
|
||||
"Hello": "XXX",
|
||||
"Hello {username}": "XXX",
|
||||
"FOO": "Foo",
|
||||
"BAR {something}": "Bar {something}"
|
||||
};
|
||||
|
||||
const translationsFallback = {
|
||||
"MMM-Module": {
|
||||
"Hello": "XXX",
|
||||
"Hello {username}": "XXX",
|
||||
"FOO": "XXX",
|
||||
"BAR {something}": "XXX",
|
||||
"A key": "A translation"
|
||||
}
|
||||
};
|
||||
|
||||
const coreTranslationsFallback = {
|
||||
"FOO": "XXX",
|
||||
"BAR {something}": "XXX",
|
||||
"Hello": "XXX",
|
||||
"Hello {username}": "XXX",
|
||||
"A key": "XXX",
|
||||
"Fallback": "core fallback"
|
||||
};
|
||||
|
||||
function setTranslations(Translator) {
|
||||
Translator.translations = translations;
|
||||
Translator.coreTranslations = coreTranslations;
|
||||
Translator.translationsFallback = translationsFallback;
|
||||
Translator.coreTranslationsFallback = coreTranslationsFallback;
|
||||
}
|
||||
|
||||
it("should return custom module translation", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
let translation = Translator.translate({name: "MMM-Module"}, "Hello");
|
||||
expect(translation).to.be.equal("Hallo");
|
||||
translation = Translator.translate({name: "MMM-Module"}, "Hello {username}", {username: "fewieden"});
|
||||
expect(translation).to.be.equal("Hallo fewieden");
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should return core translation", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
let translation = Translator.translate({name: "MMM-Module"}, "FOO");
|
||||
expect(translation).to.be.equal("Foo");
|
||||
translation = Translator.translate({name: "MMM-Module"}, "BAR {something}", {something: "Lorem Ipsum"});
|
||||
expect(translation).to.be.equal("Bar Lorem Ipsum");
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should return custom module translation fallback", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({name: "MMM-Module"}, "A key");
|
||||
expect(translation).to.be.equal("A translation");
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should return core translation fallback", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({name: "MMM-Module"}, "Fallback");
|
||||
expect(translation).to.be.equal("core fallback");
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should return translation with placeholder for missing variables", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({name: "MMM-Module"}, "Hello {username}");
|
||||
expect(translation).to.be.equal("Hallo {username}");
|
||||
done();
|
||||
};
|
||||
});
|
||||
|
||||
it("should return key if no translation was found", function(done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
setTranslations(Translator);
|
||||
const translation = Translator.translate({name: "MMM-Module"}, "MISSING");
|
||||
expect(translation).to.be.equal("MISSING");
|
||||
done();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
const mmm = {
|
||||
name: "TranslationTest",
|
||||
file(file) {
|
||||
return `http://localhost:3000/translations/${file}`;
|
||||
}
|
||||
};
|
||||
|
||||
it("should load translations", function(done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
|
||||
Translator.load(mmm, file, false, function() {
|
||||
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
||||
expect(Translator.translations[mmm.name]).to.be.deep.equal(json);
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("should load translation fallbacks", function(done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
|
||||
Translator.load(mmm, file, true, function() {
|
||||
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
||||
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal(json);
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("should strip comments", function(done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
const file = "StripComments.json";
|
||||
|
||||
Translator.load(mmm, file, false, function() {
|
||||
expect(Translator.translations[mmm.name]).to.be.deep.equal({
|
||||
"FOO\"BAR": "Today",
|
||||
"N": "N",
|
||||
"E": "E",
|
||||
"S": "S",
|
||||
"W": "W"
|
||||
});
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("should not load translations, if module fallback exists", function(done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator, XMLHttpRequest} = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
|
||||
XMLHttpRequest.prototype.send = function() {
|
||||
throw "Shouldn't load files";
|
||||
};
|
||||
|
||||
Translator.translationsFallback[mmm.name] = {
|
||||
Hello: "Hallo"
|
||||
};
|
||||
|
||||
Translator.load(mmm, file, false, function() {
|
||||
expect(Translator.translations[mmm.name]).to.be.undefined;
|
||||
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal({
|
||||
Hello: "Hallo"
|
||||
});
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadCoreTranslations", function() {
|
||||
it("should load core translations and fallback", function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
Translator.loadCoreTranslations("en");
|
||||
|
||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||
setTimeout(function() {
|
||||
expect(Translator.coreTranslations).to.be.deep.equal(en);
|
||||
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||
done();
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
|
||||
it("should load core fallback if language cannot be found", function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
Translator.loadCoreTranslations("MISSINGLANG");
|
||||
|
||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||
setTimeout(function() {
|
||||
expect(Translator.coreTranslations).to.be.deep.equal({});
|
||||
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||
done();
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadCoreTranslationsFallback", function() {
|
||||
it("should load core translations fallback", function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
Translator.loadCoreTranslationsFallback();
|
||||
|
||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
||||
setTimeout(function() {
|
||||
expect(Translator.coreTranslationsFallback).to.be.deep.equal(en);
|
||||
done();
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
|
||||
it("should load core fallback if language cannot be found", function(done) {
|
||||
const dom = new JSDOM(`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously",
|
||||
resources: "usable" });
|
||||
dom.window.onload = function() {
|
||||
const {Translator} = dom.window;
|
||||
Translator.loadCoreTranslations();
|
||||
|
||||
setTimeout(function() {
|
||||
expect(Translator.coreTranslationsFallback).to.be.deep.equal({});
|
||||
done();
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
41
tests/unit/classes/utils_spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
var chai = require("chai");
|
||||
var expect = chai.expect;
|
||||
var Utils = require("../../../js/utils.js");
|
||||
var colors = require("colors/safe");
|
||||
|
||||
describe("Utils", function() {
|
||||
describe("colors", function() {
|
||||
var colorsEnabled = colors.enabled;
|
||||
|
||||
afterEach(function() {
|
||||
colors.enabled = colorsEnabled;
|
||||
});
|
||||
|
||||
it("should have info, warn and error properties", function() {
|
||||
expect(Utils.colors).to.have.property("info");
|
||||
expect(Utils.colors).to.have.property("warn");
|
||||
expect(Utils.colors).to.have.property("error");
|
||||
});
|
||||
|
||||
it("properties should be functions", function() {
|
||||
expect(Utils.colors.info).to.be.a("function");
|
||||
expect(Utils.colors.warn).to.be.a("function");
|
||||
expect(Utils.colors.error).to.be.a("function");
|
||||
});
|
||||
|
||||
it("should print colored message in supported consoles", function() {
|
||||
colors.enabled = true;
|
||||
expect(Utils.colors.info("some informations")).to.be.equal("\u001b[34msome informations\u001b[39m");
|
||||
expect(Utils.colors.warn("a warning")).to.be.equal("\u001b[33ma warning\u001b[39m");
|
||||
expect(Utils.colors.error("ERROR!")).to.be.equal("\u001b[31mERROR!\u001b[39m");
|
||||
});
|
||||
|
||||
it("should print message in unsupported consoles", function() {
|
||||
colors.enabled = false;
|
||||
expect(Utils.colors.info("some informations")).to.be.equal("some informations");
|
||||
expect(Utils.colors.warn("a warning")).to.be.equal("a warning");
|
||||
expect(Utils.colors.error("ERROR!")).to.be.equal("ERROR!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,51 +0,0 @@
|
||||
var chai = require("chai");
|
||||
var expect = chai.expect;
|
||||
var jsClass = require("../../../js/class.js");
|
||||
|
||||
describe("File js/class", function() {
|
||||
describe("Test function cloneObject", function() {
|
||||
var cloneObject = jsClass._test.cloneObject;
|
||||
|
||||
it("should be return equals object", function() {
|
||||
var expected = {name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror"};
|
||||
var obj = {};
|
||||
obj = cloneObject(expected);
|
||||
expect(expected).to.deep.equal(obj);
|
||||
});
|
||||
|
||||
it("should be return equals int", function() {
|
||||
var expected = 1;
|
||||
var obj = {};
|
||||
obj = cloneObject(expected);
|
||||
expect(expected).to.equal(obj);
|
||||
});
|
||||
|
||||
it("should be return equals string", function() {
|
||||
var expected = "Perfect stranger";
|
||||
var obj = {};
|
||||
obj = cloneObject(expected);
|
||||
expect(expected).to.equal(obj);
|
||||
});
|
||||
|
||||
it("should be return equals undefined", function() {
|
||||
var expected = undefined;
|
||||
var obj = {};
|
||||
obj = cloneObject(expected);
|
||||
expect(undefined).to.equal(obj);
|
||||
});
|
||||
|
||||
// CoverageME
|
||||
/*
|
||||
context("Test lockstring code", function() {
|
||||
it("should be return equals object", function() {
|
||||
var expected = {name: "Module", lockStrings: "stringLock"};
|
||||
var obj = {};
|
||||
obj = cloneObject(expected);
|
||||
expect(expected).to.deep.equal(obj);
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -1,20 +1,32 @@
|
||||
var chai = require("chai");
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,118 +0,0 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var chai = require("chai");
|
||||
var expect = chai.expect;
|
||||
var mlog = require("mocha-logger");
|
||||
|
||||
describe("Translations have the same keys as en.js", function() {
|
||||
var translations = require("../../../translations/translations.js");
|
||||
var base = JSON.parse(stripComments(fs.readFileSync("translations/en.json", "utf8")));
|
||||
var baseKeys = Object.keys(base).sort();
|
||||
|
||||
Object.keys(translations).forEach(function(tr) {
|
||||
var fileName = translations[tr];
|
||||
var fileContent = stripComments(fs.readFileSync(fileName, "utf8"));
|
||||
var fileTranslations = JSON.parse(fileContent);
|
||||
var fileKeys = Object.keys(fileTranslations).sort();
|
||||
|
||||
it(fileName + " keys should be in base", function() {
|
||||
fileKeys.forEach(function(key) {
|
||||
expect( baseKeys.indexOf(key) ).to.be.at.least(0);
|
||||
});
|
||||
});
|
||||
|
||||
it(fileName + " should contain all base keys", function() {
|
||||
var test = this;
|
||||
baseKeys.forEach(function(key) {
|
||||
// TODO: when all translations are fixed, use
|
||||
// expect(fileKeys).to.deep.equal(baseKeys);
|
||||
// instead of the try-catch-block
|
||||
|
||||
try {
|
||||
expect(fileKeys).to.deep.equal(baseKeys);
|
||||
} catch(e) {
|
||||
if (e instanceof chai.AssertionError) {
|
||||
diff = baseKeys.filter(function(x) { return fileKeys.indexOf(x) < 0 });
|
||||
mlog.pending("Missing Translations for language " + tr + ": ", diff);
|
||||
test.skip();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Copied from js/translator.js
|
||||
function stripComments(str, opts) {
|
||||
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
|
||||
|
||||
var singleComment = 1;
|
||||
var multiComment = 2;
|
||||
|
||||
function stripWithoutWhitespace() {
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripWithWhitespace(str, start, end) {
|
||||
return str.slice(start, end).replace(/\S/g, " ");
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
var currentChar;
|
||||
var nextChar;
|
||||
var insideString = false;
|
||||
var insideComment = false;
|
||||
var offset = 0;
|
||||
var ret = "";
|
||||
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
|
||||
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
currentChar = str[i];
|
||||
nextChar = str[i + 1];
|
||||
|
||||
if (!insideComment && currentChar === "\"") {
|
||||
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
|
||||
if (!escaped) {
|
||||
insideString = !insideString;
|
||||
}
|
||||
}
|
||||
|
||||
if (insideString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideComment && currentChar + nextChar === "//") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = singleComment;
|
||||
i++;
|
||||
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
continue;
|
||||
} else if (insideComment === singleComment && currentChar === "\n") {
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
} else if (!insideComment && currentChar + nextChar === "/*") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = multiComment;
|
||||
i++;
|
||||
continue;
|
||||
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i + 1);
|
||||
offset = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
|
||||
}
|
@@ -25,6 +25,7 @@
|
||||
"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_SINGLE": "Die huidige installasie is {COMMIT_COUNT} commit agter op die {BRANCH_NAME} branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "Die huidige installasie is {COMMIT_COUNT} commits agter op die {BRANCH_NAME} branch."
|
||||
}
|
||||
|
33
translations/bg.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"LOADING": "Зареждане …",
|
||||
|
||||
"TODAY": "Днес",
|
||||
"TOMORROW": "Утре",
|
||||
"DAYAFTERTOMORROW": "Вдругиден",
|
||||
"RUNNING": "Свършва на",
|
||||
"EMPTY": "Няма предстоящи събития.",
|
||||
|
||||
"WEEK": "Седмица {weekNumber}",
|
||||
|
||||
"N": "С",
|
||||
"NNE": "ССИ",
|
||||
"NE": "СИ",
|
||||
"ENE": "ИСИ",
|
||||
"E": "И",
|
||||
"ESE": "ИЮИ",
|
||||
"SE": "ЮИ",
|
||||
"SSE": "ЮЮИ",
|
||||
"S": "Ю",
|
||||
"SSW": "ЮЮЗ",
|
||||
"SW": "ЮЗ",
|
||||
"WSW": "ЗЮЗ",
|
||||
"W": "З",
|
||||
"WNW": "ЗСЗ",
|
||||
"NW": "СЗ",
|
||||
"NNW": "ССЗ",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Налична актуализация за MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за {MODULE_NAME} модул.",
|
||||
"UPDATE_INFO_SINGLE": "Текущата инсталация е изостанала с {COMMIT_COUNT} commit къмита на клон {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "Текущата инсталация е изостанала с {COMMIT_COUNT} commits къмита на клон {BRANCH_NAME}."
|
||||
}
|
33
translations/ca.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"LOADING": "Carregant …",
|
||||
|
||||
"TODAY": "Avui",
|
||||
"TOMORROW": "Demà",
|
||||
"DAYAFTERTOMORROW": "Demà passat",
|
||||
"RUNNING": "Acaba en",
|
||||
"EMPTY": "No hi ha esdeveniments programats.",
|
||||
|
||||
"WEEK": "Setmana",
|
||||
|
||||
"N": "N",
|
||||
"NNE": "NNE",
|
||||
"NE": "NE",
|
||||
"ENE": "ENE",
|
||||
"E": "E",
|
||||
"ESE": "ESE",
|
||||
"SE": "SE",
|
||||
"SSE": "SSE",
|
||||
"S": "S",
|
||||
"SSW": "SSO",
|
||||
"SW": "SO",
|
||||
"WSW": "OSO",
|
||||
"W": "O",
|
||||
"WNW": "ONO",
|
||||
"NW": "NO",
|
||||
"NNW": "NNO",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² actualizació disponible.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualizació per al mòdul {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "La teva instal·lació actual està {COMMIT_COUNT} commit canvis darrere de la branca {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "La teva instal·lació actual està {COMMIT_COUNT} commits canvis darrere de la branca {BRANCH_NAME}."
|
||||
}
|
33
translations/cs.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"LOADING": "Načítání …",
|
||||
|
||||
"TODAY": "Dnes",
|
||||
"TOMORROW": "Zítra",
|
||||
"DAYAFTERTOMORROW": "Pozítří",
|
||||
"RUNNING": "Končí za",
|
||||
"EMPTY": "Žádné nadcházející události.",
|
||||
|
||||
"WEEK": "{weekNumber}. týden",
|
||||
|
||||
"N": "S",
|
||||
"NNE": "SSV",
|
||||
"NE": "SV",
|
||||
"ENE": "VSV",
|
||||
"E": "V",
|
||||
"ESE": "VJV",
|
||||
"SE": "JV",
|
||||
"SSE": "JJV",
|
||||
"S": "J",
|
||||
"SSW": "JJZ",
|
||||
"SW": "JZ",
|
||||
"WSW": "ZJZ",
|
||||
"W": "Z",
|
||||
"WNW": "ZSZ",
|
||||
"NW": "SZ",
|
||||
"NNW": "SSZ",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Dostupná aktualizace pro MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Dostupná aktualizace pro modul {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "Současná instalace je na větvi {BRANCH_NAME} pozadu o {COMMIT_COUNT} commit.",
|
||||
"UPDATE_INFO_MULTIPLE": "Současná instalace je na větvi {BRANCH_NAME} pozadu o {COMMIT_COUNT} commits."
|
||||
}
|
@@ -7,7 +7,7 @@
|
||||
"RUNNING": "Gorffen mewn",
|
||||
"EMPTY": "Dim digwyddiadau.",
|
||||
|
||||
"WEEK": "Wythnos",
|
||||
"WEEK": "Wythnos {weekNumber}",
|
||||
|
||||
"N": "Go",
|
||||
"NNE": "GoGoDw",
|
||||
@@ -27,6 +27,7 @@
|
||||
"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_SINGLE": "Mae'r fersiwn bresenol {COMMIT_COUNT} commit tu ôl i'r gangen {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "Mae'r fersiwn bresenol {COMMIT_COUNT} commit tu ôl i'r gangen {BRANCH_NAME}."
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@
|
||||
"DAYAFTERTOMORROW": "I overmorgen",
|
||||
"RUNNING": "Slutter om",
|
||||
"EMPTY": "Ingen kommende begivenheder.",
|
||||
"FEELS": "Føles som",
|
||||
"WEEK": "Uge {weekNumber}",
|
||||
|
||||
"N": "N",
|
||||
"NNE": "NNØ",
|
||||
@@ -26,6 +28,7 @@
|
||||
|
||||
|
||||
"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_SINGLE": "Den nuværende installation er {COMMIT_COUNT} commit bagud på {BRANCH_NAME} branch'en.",
|
||||
"UPDATE_INFO_MULTIPLE": "Den nuværende installation er {COMMIT_COUNT} commits bagud på {BRANCH_NAME} branch'en."
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"RUNNING": "noch",
|
||||
"EMPTY": "Keine Termine.",
|
||||
|
||||
"WEEK": "Woche",
|
||||
"WEEK": "{weekNumber}. Kalenderwoche",
|
||||
|
||||
"N": "N",
|
||||
"NNE": "NNO",
|
||||
@@ -27,6 +27,9 @@
|
||||
"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_SINGLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commit hinter dem {BRANCH_NAME} Branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commits hinter dem {BRANCH_NAME} Branch.",
|
||||
|
||||
"FEELS": "Gefühlt"
|
||||
}
|
||||
|