mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-12 09:52:37 +00:00
Compare commits
837 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343e7de7bd | ||
|
|
e87f50e64a | ||
|
|
abe5c08a52 | ||
|
|
f14e956166 | ||
|
|
2eaf9dfeeb | ||
|
|
0300ce05d5 | ||
|
|
9e0293047f | ||
|
|
298e585361 | ||
|
|
21a6d1bcea | ||
|
|
bbe17b9b01 | ||
|
|
4381fc6695 | ||
|
|
818fd7b490 | ||
|
|
4c6c6f9ed3 | ||
|
|
2338a90191 | ||
|
|
6cad0e191b | ||
|
|
f23e604ed4 | ||
|
|
0c1abad9df | ||
|
|
fb96cc3c72 | ||
|
|
e917f40542 | ||
|
|
29d467715f | ||
|
|
b791a3761f | ||
|
|
02201d9f15 | ||
|
|
b8dbf95497 | ||
|
|
65022f3ce1 | ||
|
|
44854d6a4f | ||
|
|
203014c654 | ||
|
|
d3e53586fd | ||
|
|
9dd343054e | ||
|
|
11d17dd2c0 | ||
|
|
4cc3e481cc | ||
|
|
174da38cc8 | ||
|
|
0a35505e8d | ||
|
|
032f7ac299 | ||
|
|
ca906c4b36 | ||
|
|
50f72f09ac | ||
|
|
02cf9b37e2 | ||
|
|
6f273d76b3 | ||
|
|
0a1067ec7d | ||
|
|
48756e8774 | ||
|
|
4915ad8fc7 | ||
|
|
ba128cbae9 | ||
|
|
2a6e51493e | ||
|
|
ef678f9f8a | ||
|
|
e997ee7071 | ||
|
|
1273390af3 | ||
|
|
0f6eb4a244 | ||
|
|
2c0c62c89b | ||
|
|
ec13c952d9 | ||
|
|
6fc71cf6f8 | ||
|
|
bd3137a3dc | ||
|
|
b26da4f97f | ||
|
|
cde0adc28e | ||
|
|
2813f101b8 | ||
|
|
4d8fb8c176 | ||
|
|
9ae62d60f7 | ||
|
|
17c14e137e | ||
|
|
bc239f6608 | ||
|
|
6493fad8a4 | ||
|
|
d539f459ca | ||
|
|
2cca6a2f39 | ||
|
|
80e07bae0d | ||
|
|
d4c4f6e1a5 | ||
|
|
d24fe4e983 | ||
|
|
aaa9042810 | ||
|
|
a4bb1cefb9 | ||
|
|
c3339b47bb | ||
|
|
0c1e5ea881 | ||
|
|
3fbd9006ad | ||
|
|
be9761146c | ||
|
|
5aa9e7e0f6 | ||
|
|
65e87aea52 | ||
|
|
66fffc932c | ||
|
|
1e934e16af | ||
|
|
82fbb7e32d | ||
|
|
8bf9b9bef9 | ||
|
|
2d15e4f976 | ||
|
|
055ce56a57 | ||
|
|
f1f2a61dc8 | ||
|
|
39c1b37726 | ||
|
|
5b1b25fa86 | ||
|
|
54ab0b1bf0 | ||
|
|
5507e9ffe9 | ||
|
|
30d5bfe59e | ||
|
|
b716ec33d9 | ||
|
|
e25209d9f9 | ||
|
|
1f4ac82495 | ||
|
|
0baf58f3fd | ||
|
|
9a769203e3 | ||
|
|
1435efaea7 | ||
|
|
c411ac821e | ||
|
|
da0489fc0c | ||
|
|
c82de1e314 | ||
|
|
604a555e14 | ||
|
|
87e011ff96 | ||
|
|
9b2051827c | ||
|
|
47aefb0c82 | ||
|
|
70da1d6f5c | ||
|
|
c0743ce9de | ||
|
|
fe7b4044e0 | ||
|
|
701ce8ad47 | ||
|
|
591f907134 | ||
|
|
4f1db749c0 | ||
|
|
ae72ed8c67 | ||
|
|
ab7934fa98 | ||
|
|
a4c77f0c9e | ||
|
|
0023c64d59 | ||
|
|
b55b3bd63b | ||
|
|
cbda20f67e | ||
|
|
e598dbb206 | ||
|
|
094881fc5c | ||
|
|
b13414cab8 | ||
|
|
3e65ac653b | ||
|
|
05621c9876 | ||
|
|
938619ce0c | ||
|
|
8e5267d44f | ||
|
|
3b55886c45 | ||
|
|
ed3aceb427 | ||
|
|
c3b2aaec69 | ||
|
|
bb1d3431cc | ||
|
|
fe83fe338a | ||
|
|
6504b5e818 | ||
|
|
8578900bfb | ||
|
|
d730dd04bf | ||
|
|
1d90c5e1fe | ||
|
|
0f39b7733c | ||
|
|
038b6765e7 | ||
|
|
58569a648c | ||
|
|
df0f048ecc | ||
|
|
288a008e72 | ||
|
|
439690b981 | ||
|
|
b4e75f6844 | ||
|
|
3f6a5a7772 | ||
|
|
a2d7cdcfb4 | ||
|
|
d995ce38ea | ||
|
|
0b21a22027 | ||
|
|
111d75b351 | ||
|
|
b54cac72ec | ||
|
|
69b2dd698c | ||
|
|
16bf19d115 | ||
|
|
c57c9c2a8f | ||
|
|
9f750e5980 | ||
|
|
8d296e563d | ||
|
|
88f7570caf | ||
|
|
00a7c6b5be | ||
|
|
612b06346d | ||
|
|
9620566fbb | ||
|
|
ed65c70a36 | ||
|
|
cdd87436be | ||
|
|
a143ebc8e0 | ||
|
|
93581ca4d9 | ||
|
|
c5e9c9a683 | ||
|
|
1bdab10872 | ||
|
|
eca339ad60 | ||
|
|
af5eb3447f | ||
|
|
b72cb52a71 | ||
|
|
d0565a15d3 | ||
|
|
468bf1d6a0 | ||
|
|
1d2637c980 | ||
|
|
a52c68d852 | ||
|
|
e12f57d872 | ||
|
|
5def02ff7f | ||
|
|
7cab6eb680 | ||
|
|
dcaa3526a4 | ||
|
|
2a989bc235 | ||
|
|
d588fab981 | ||
|
|
9cb006b871 | ||
|
|
9056abaf4a | ||
|
|
3c27fd10b6 | ||
|
|
4048d79fc5 | ||
|
|
212e60c12d | ||
|
|
45142bc1bc | ||
|
|
e791a663c8 | ||
|
|
81ae95eba7 | ||
|
|
7ca5d81123 | ||
|
|
e234d2379b | ||
|
|
d0838d53c2 | ||
|
|
880e2160a3 | ||
|
|
c214d776df | ||
|
|
0a8220ca52 | ||
|
|
b0c57272c2 | ||
|
|
fbdebcae8a | ||
|
|
8e376deaaa | ||
|
|
2d353ffa35 | ||
|
|
b70a9c36fa | ||
|
|
c98967cb1e | ||
|
|
05f0d1855c | ||
|
|
cee5043625 | ||
|
|
528358f3c3 | ||
|
|
da90412cea | ||
|
|
e794aab012 | ||
|
|
69daf14ffd | ||
|
|
afa6c4270d | ||
|
|
be22309e45 | ||
|
|
6f27e5ae07 | ||
|
|
7e79547973 | ||
|
|
a5668b1b99 | ||
|
|
f04dd6b6cd | ||
|
|
9f9c17ea8a | ||
|
|
48845ab60e | ||
|
|
59bc2318f8 | ||
|
|
c622db918b | ||
|
|
4b381f33b5 | ||
|
|
7cfc7b9d74 | ||
|
|
7f52f9bcf2 | ||
|
|
c4e7e42cdd | ||
|
|
d181365620 | ||
|
|
97b474665a | ||
|
|
284b01c524 | ||
|
|
e3857ca0f4 | ||
|
|
0aee42255f | ||
|
|
fcb0d33e29 | ||
|
|
3a761f2082 | ||
|
|
ed0ad7b988 | ||
|
|
c392b5a661 | ||
|
|
766848eea3 | ||
|
|
7ede537d1b | ||
|
|
ec38f90662 | ||
|
|
d793b82d51 | ||
|
|
aafeb1e875 | ||
|
|
480c10b239 | ||
|
|
82bb2544de | ||
|
|
b714abd9a0 | ||
|
|
c1be0180f9 | ||
|
|
2cfafe7bfe | ||
|
|
da8edbfdc1 | ||
|
|
54bcbbc1de | ||
|
|
5463183e01 | ||
|
|
42b80b18f8 | ||
|
|
65fada7ef1 | ||
|
|
9e4997aa81 | ||
|
|
e8c6ef945a | ||
|
|
14a22efae1 | ||
|
|
f7f0bc8681 | ||
|
|
9cb8a135e6 | ||
|
|
a02bce6517 | ||
|
|
9604c3a187 | ||
|
|
1ba4213910 | ||
|
|
6c63fac240 | ||
|
|
b3dd531abb | ||
|
|
4ce42d4f70 | ||
|
|
4325fcdca2 | ||
|
|
8cf9d5f3ed | ||
|
|
8cb6015930 | ||
|
|
f3cefde7cb | ||
|
|
92fcdde60d | ||
|
|
2dcf55ea89 | ||
|
|
8537f40f1d | ||
|
|
8417590893 | ||
|
|
4c43f5db15 | ||
|
|
eb32ec89b4 | ||
|
|
ad66c02735 | ||
|
|
e8c0611db4 | ||
|
|
7fa54642e1 | ||
|
|
0e6d40f96c | ||
|
|
cd1fe4e182 | ||
|
|
c56b601ab8 | ||
|
|
e78fa11fd4 | ||
|
|
1619dd29e9 | ||
|
|
793f3dd75c | ||
|
|
ce05f8e958 | ||
|
|
c8c4585946 | ||
|
|
990d1adfb2 | ||
|
|
404343de1d | ||
|
|
24bfaaca7e | ||
|
|
060ca43fc8 | ||
|
|
b87e802c8d | ||
|
|
0f596d5620 | ||
|
|
3ebdb67bc0 | ||
|
|
a5668ef729 | ||
|
|
24fccf6c44 | ||
|
|
e12692252e | ||
|
|
a6cbc9f0ef | ||
|
|
be073daaf2 | ||
|
|
4c919a7489 | ||
|
|
7a7ed48063 | ||
|
|
01010fe795 | ||
|
|
32382dc461 | ||
|
|
bf83341fb9 | ||
|
|
c4d2a7b409 | ||
|
|
4ad832dcdd | ||
|
|
b35d25eda4 | ||
|
|
da51a5512f | ||
|
|
710d51bf32 | ||
|
|
446bb229bc | ||
|
|
c605023bad | ||
|
|
28d866c001 | ||
|
|
95b587381b | ||
|
|
fc14431147 | ||
|
|
5d8f2f5da1 | ||
|
|
cf428dc1f7 | ||
|
|
c579989ded | ||
|
|
a954a62762 | ||
|
|
855860c00c | ||
|
|
58c48b1b21 | ||
|
|
512c392386 | ||
|
|
c5ed70dbfc | ||
|
|
f75ca0c399 | ||
|
|
75c2977daf | ||
|
|
b3ef4b40c5 | ||
|
|
bd5664cf8e | ||
|
|
272219eb61 | ||
|
|
acbcb47739 | ||
|
|
672575e427 | ||
|
|
be0b3b1d63 | ||
|
|
c88d25b4ee | ||
|
|
9b83862a96 | ||
|
|
21c7f0da6e | ||
|
|
dca5759569 | ||
|
|
0abf87bfa2 | ||
|
|
6b8b37843b | ||
|
|
fa0b83f056 | ||
|
|
a706fa3590 | ||
|
|
852aa3d260 | ||
|
|
c4edcaad87 | ||
|
|
9d0cab01d5 | ||
|
|
c3e507234d | ||
|
|
405ec82dd0 | ||
|
|
9c4c77fe84 | ||
|
|
831afdf9e7 | ||
|
|
1996efb183 | ||
|
|
19bb2a0238 | ||
|
|
af6cf70558 | ||
|
|
9b57e6049e | ||
|
|
f54c06fb94 | ||
|
|
2107e7c427 | ||
|
|
eae277f165 | ||
|
|
a9c4ad2895 | ||
|
|
e88ba1ab1c | ||
|
|
677dcb87ef | ||
|
|
7bb217636e | ||
|
|
abc16e98eb | ||
|
|
3b1405609e | ||
|
|
b0d3518c1d | ||
|
|
aed005618e | ||
|
|
559970c95d | ||
|
|
f6f3aa11ea | ||
|
|
472c91f022 | ||
|
|
4a7f498683 | ||
|
|
2b87d6ec01 | ||
|
|
1af282a7a1 | ||
|
|
46b63f52fe | ||
|
|
01977005fb | ||
|
|
b1a46b365b | ||
|
|
3a0a02d3ba | ||
|
|
1acbef5bfa | ||
|
|
66b2ba07bb | ||
|
|
4777538103 | ||
|
|
31d31fc3d3 | ||
|
|
21f606c6ba | ||
|
|
6508ec2d17 | ||
|
|
c623ca9fe0 | ||
|
|
90deaf564f | ||
|
|
e2d2cf67fa | ||
|
|
8ee73a11bb | ||
|
|
b5b01be373 | ||
|
|
730f2eb0b8 | ||
|
|
4576754de2 | ||
|
|
0fb9e0bc89 | ||
|
|
000c34ef73 | ||
|
|
0ec80a7791 | ||
|
|
e9650285bd | ||
|
|
d0cc0a4034 | ||
|
|
f9c4a3a9c0 | ||
|
|
15fd2021bb | ||
|
|
75cf1d610e | ||
|
|
8f5ee9466a | ||
|
|
467720f1c4 | ||
|
|
026e624e23 | ||
|
|
460a9fc5f7 | ||
|
|
3695e64ce9 | ||
|
|
b3bb829e4d | ||
|
|
1b9f8c23d2 | ||
|
|
5ee5fd7332 | ||
|
|
18d94b9a26 | ||
|
|
3ae9686b2b | ||
|
|
bbe4ef8497 | ||
|
|
a2fd354dc9 | ||
|
|
a7202078ce | ||
|
|
d8940a9cea | ||
|
|
96611333bf | ||
|
|
5074123f57 | ||
|
|
5bfbd3eaa0 | ||
|
|
7fdeed14f5 | ||
|
|
04a9ca92b5 | ||
|
|
016a1e9adb | ||
|
|
1d12e57606 | ||
|
|
cb753f5371 | ||
|
|
7a9f9b2705 | ||
|
|
6d4b4dd9fc | ||
|
|
3d9c92fb63 | ||
|
|
d4168f6b5d | ||
|
|
1751cabb9d | ||
|
|
b0e3b6414a | ||
|
|
51967ed9f5 | ||
|
|
fc06f13c30 | ||
|
|
b094707324 | ||
|
|
e4a0a517d5 | ||
|
|
222a5f3779 | ||
|
|
72f7106086 | ||
|
|
33387b60cc | ||
|
|
83cc18f648 | ||
|
|
1735ca57d5 | ||
|
|
a49459b253 | ||
|
|
5a4fbbf48a | ||
|
|
f7465679c0 | ||
|
|
2a5299ebcb | ||
|
|
b3bddb2c99 | ||
|
|
997aec8cc2 | ||
|
|
c67320f185 | ||
|
|
8224a6ac35 | ||
|
|
abcee8aa56 | ||
|
|
23c6b44921 | ||
|
|
1034171e91 | ||
|
|
bf49f79e6e | ||
|
|
f7f24dbdfe | ||
|
|
31ec848aec | ||
|
|
9eb08420b6 | ||
|
|
eb6d8d4f83 | ||
|
|
e10f620cf9 | ||
|
|
f750436b64 | ||
|
|
a4a8504558 | ||
|
|
385e5aabaa | ||
|
|
d831315e20 | ||
|
|
e0906f3462 | ||
|
|
6595b6a44f | ||
|
|
0183d7a080 | ||
|
|
89e803ee42 | ||
|
|
c0ce52abe3 | ||
|
|
a1c7f20990 | ||
|
|
0ef6f89d44 | ||
|
|
54b04962a8 | ||
|
|
60f8de282d | ||
|
|
b4350278a0 | ||
|
|
332e429a41 | ||
|
|
7be25c21ed | ||
|
|
1dfa5d383c | ||
|
|
c2d2c278e0 | ||
|
|
75a57829c2 | ||
|
|
c3c5307624 | ||
|
|
879d585f2e | ||
|
|
9969fede35 | ||
|
|
c15b31b374 | ||
|
|
a3a6c33b32 | ||
|
|
236bf6e0fc | ||
|
|
974de179e0 | ||
|
|
60e03777f3 | ||
|
|
05f6b2510f | ||
|
|
f3274977f5 | ||
|
|
cf7fb1a3b9 | ||
|
|
12457a87d4 | ||
|
|
9a8de7db80 | ||
|
|
68e02a528e | ||
|
|
277055f44e | ||
|
|
c3fc745c7e | ||
|
|
8901ed219d | ||
|
|
d7c70dc021 | ||
|
|
53c789bff9 | ||
|
|
eb63745664 | ||
|
|
91d72e48ad | ||
|
|
1dcda63192 | ||
|
|
3ea6544f77 | ||
|
|
d12a587f11 | ||
|
|
2b147bb98b | ||
|
|
6529eaaf9a | ||
|
|
a68aa148b8 | ||
|
|
98942d6f9c | ||
|
|
690efc0aff | ||
|
|
627cfa1dff | ||
|
|
99aca932db | ||
|
|
dd43f35bbe | ||
|
|
093988e136 | ||
|
|
087a472765 | ||
|
|
ce13d7f98b | ||
|
|
b1fc766908 | ||
|
|
22384342db | ||
|
|
badce5146a | ||
|
|
0bf3ff9c17 | ||
|
|
860840c367 | ||
|
|
221b6325f6 | ||
|
|
06389e35f9 | ||
|
|
a7756cec13 | ||
|
|
9ee11654a6 | ||
|
|
a273266e5e | ||
|
|
e2158716d6 | ||
|
|
c132206543 | ||
|
|
f49312ed13 | ||
|
|
a9f69f07e6 | ||
|
|
d7429a4812 | ||
|
|
be76d5ce9a | ||
|
|
f2bc10c5c0 | ||
|
|
43eb760bce | ||
|
|
a7684e3e9f | ||
|
|
8949aa3bec | ||
|
|
e40ddd4b69 | ||
|
|
17637fb1f6 | ||
|
|
f71defe958 | ||
|
|
b8d6a6da1f | ||
|
|
fbc886b21c | ||
|
|
8879fb55de | ||
|
|
ed316e8bf3 | ||
|
|
45529f7de9 | ||
|
|
dbdff38d2e | ||
|
|
21c3179e03 | ||
|
|
c05d93aed8 | ||
|
|
6225abb010 | ||
|
|
c41fff8f5c | ||
|
|
8589d9c482 | ||
|
|
7f264953af | ||
|
|
cfff2ad72b | ||
|
|
c0258b352e | ||
|
|
3e1b051ec3 | ||
|
|
b34bb87d7a | ||
|
|
83b8cc6729 | ||
|
|
878c0be727 | ||
|
|
e7f06f5c0c | ||
|
|
a1fc38c5fe | ||
|
|
ff0ab24000 | ||
|
|
56a10d192d | ||
|
|
1a8413d8f0 | ||
|
|
934b156ebb | ||
|
|
f9639d9705 | ||
|
|
4c345c4f33 | ||
|
|
490151267a | ||
|
|
3d19a08cc7 | ||
|
|
385c4c32f9 | ||
|
|
3a5052c871 | ||
|
|
f84f590f1d | ||
|
|
5b9eba7819 | ||
|
|
cd18794fca | ||
|
|
ae3d552ad7 | ||
|
|
be5f71f4a7 | ||
|
|
745a5f0376 | ||
|
|
99114b2a61 | ||
|
|
df9bd2b0f9 | ||
|
|
e194b559ac | ||
|
|
af5344dccd | ||
|
|
2d7b8121d7 | ||
|
|
0297450702 | ||
|
|
6b17f6aa28 | ||
|
|
8a7abfe42d | ||
|
|
dd5041395c | ||
|
|
36d6a5bc15 | ||
|
|
2619f92d09 | ||
|
|
53720ae8ae | ||
|
|
bcff953fbb | ||
|
|
bcc0cc599d | ||
|
|
a1e3fed312 | ||
|
|
399dca2ef9 | ||
|
|
2e44e1626d | ||
|
|
39aa2dfe01 | ||
|
|
099929c677 | ||
|
|
af5d132410 | ||
|
|
79acbc3a98 | ||
|
|
e75e4e2284 | ||
|
|
9aa0af4f9c | ||
|
|
50e272efba | ||
|
|
209e049893 | ||
|
|
bbb3accf0c | ||
|
|
179989aa42 | ||
|
|
2881d19d43 | ||
|
|
7cfc3458ec | ||
|
|
659e1da79d | ||
|
|
a7ae79493d | ||
|
|
8b484ee707 | ||
|
|
d617d4aa09 | ||
|
|
99c04648b4 | ||
|
|
f945d50c0d | ||
|
|
e9fabd59ed | ||
|
|
ad13de3588 | ||
|
|
aad8141e27 | ||
|
|
26a76f80d6 | ||
|
|
53ead2087f | ||
|
|
faee811d67 | ||
|
|
b9c739df1f | ||
|
|
6e124842e8 | ||
|
|
7a5928ea24 | ||
|
|
7fdf7de11c | ||
|
|
b75eedb84e | ||
|
|
3b92ae49a9 | ||
|
|
775d1091db | ||
|
|
1f77b491fc | ||
|
|
eff2fd7cc0 | ||
|
|
58d2a0d874 | ||
|
|
ea9def997a | ||
|
|
a222c58047 | ||
|
|
39a838c2ab | ||
|
|
cfc0bcd5ad | ||
|
|
3418c9b50f | ||
|
|
e686611890 | ||
|
|
ebb5dee1fc | ||
|
|
d9edaffd9c | ||
|
|
cbe7b1a5b9 | ||
|
|
e758fd4093 | ||
|
|
14a99a3b25 | ||
|
|
1ba67506a0 | ||
|
|
2a6ca5d5ac | ||
|
|
aa12e6495a | ||
|
|
9269848f66 | ||
|
|
a71e61cd30 | ||
|
|
8be4604c97 | ||
|
|
a7bba903f5 | ||
|
|
1d8af5835d | ||
|
|
189c01fc74 | ||
|
|
e3a5bbf661 | ||
|
|
ee23c5f72c | ||
|
|
a2083be76b | ||
|
|
a1c4be83d6 | ||
|
|
d2fde2bfc8 | ||
|
|
e8956b0b55 | ||
|
|
2af4009a93 | ||
|
|
a5f7c946cc | ||
|
|
298542b531 | ||
|
|
fca6707a29 | ||
|
|
10d3a284e9 | ||
|
|
1a244726aa | ||
|
|
044935a164 | ||
|
|
99e5edf2c5 | ||
|
|
b26270bd13 | ||
|
|
65a8cb9ddb | ||
|
|
ba4b976e80 | ||
|
|
297ae1dbaf | ||
|
|
214614f740 | ||
|
|
0e14d3d6e8 | ||
|
|
67011c0c32 | ||
|
|
16bbb42b8d | ||
|
|
b85ac91e6c | ||
|
|
66759a33fa | ||
|
|
bf5e83861c | ||
|
|
d56a6fb06f | ||
|
|
5e7aa8e16d | ||
|
|
bace0ad339 | ||
|
|
6014eaf8eb | ||
|
|
3e96e8b3f5 | ||
|
|
95d1b8a6d0 | ||
|
|
0ecb66c99e | ||
|
|
af52b91799 | ||
|
|
3c50d6c30a | ||
|
|
2722c72c43 | ||
|
|
d5ab3101c6 | ||
|
|
32df76bdff | ||
|
|
68a06c3d1d | ||
|
|
1b42dc779b | ||
|
|
cedffd40f2 | ||
|
|
cdc8db4837 | ||
|
|
a0ee23d84e | ||
|
|
49d2d8c9d0 | ||
|
|
63620aa811 | ||
|
|
d5b11a1dba | ||
|
|
4a63af0490 | ||
|
|
a68019293f | ||
|
|
6b1c91f0dd | ||
|
|
ea93785581 | ||
|
|
32819c4fd5 | ||
|
|
fc5a438cdc | ||
|
|
57fe94f945 | ||
|
|
aa3a3bdf16 | ||
|
|
db89da3daa | ||
|
|
d6ba5796ce | ||
|
|
3968743b28 | ||
|
|
20c6226b84 | ||
|
|
463ce394fe | ||
|
|
1faefebe42 | ||
|
|
e2c9339ec4 | ||
|
|
4b1c7da171 | ||
|
|
bdfd6e5e9f | ||
|
|
4c8508b0a9 | ||
|
|
06b3f92963 | ||
|
|
d43a57af36 | ||
|
|
aeefe28710 | ||
|
|
e9de961a23 | ||
|
|
dcec778e02 | ||
|
|
b212641069 | ||
|
|
90aa50bb11 | ||
|
|
a6879e853b | ||
|
|
37fab7ac63 | ||
|
|
8b01ae08c5 | ||
|
|
b1cdf42790 | ||
|
|
974968d238 | ||
|
|
bf467cbba5 | ||
|
|
536aa2e96e | ||
|
|
3d84344b75 | ||
|
|
1054ba3b1e | ||
|
|
aa8ddb9a92 | ||
|
|
fcfe57e5e2 | ||
|
|
c4fd4e0317 | ||
|
|
3c76933824 | ||
|
|
fa83819bee | ||
|
|
b65ae88879 | ||
|
|
96db21f9bf | ||
|
|
6d356ff770 | ||
|
|
21790b32bf | ||
|
|
159f3d0aa2 | ||
|
|
012a7b0678 | ||
|
|
6595c85671 | ||
|
|
cf5c0464fe | ||
|
|
e31450f731 | ||
|
|
3653984a95 | ||
|
|
0c0b856c37 | ||
|
|
69f1b153ea | ||
|
|
43ba4bd00e | ||
|
|
bf5edcaac6 | ||
|
|
ac51709211 | ||
|
|
e1a578e819 | ||
|
|
591c9e53b0 | ||
|
|
87d543eb3a | ||
|
|
40c1521591 | ||
|
|
b31c2a6264 | ||
|
|
66a42f13f1 | ||
|
|
8de6ebbbd1 | ||
|
|
649de694ed | ||
|
|
8a52fde8fc | ||
|
|
fb8bd657de | ||
|
|
b04a0a6b61 | ||
|
|
f29c911a0f | ||
|
|
bd908123c2 | ||
|
|
cbdb0b67ab | ||
|
|
aa3848f420 | ||
|
|
6cf0748172 | ||
|
|
11122d3f81 | ||
|
|
2dea9398f2 | ||
|
|
de93b3294f | ||
|
|
7accb84eb9 | ||
|
|
ea90ed04d6 | ||
|
|
838eed2630 | ||
|
|
376b65c749 | ||
|
|
3b4432cb00 | ||
|
|
7bc71029de | ||
|
|
d736dd92be | ||
|
|
6eba8d681c | ||
|
|
5fe654c19d | ||
|
|
dd366f35a8 | ||
|
|
2ababa521d | ||
|
|
bda8f26511 | ||
|
|
7f1a3df25b | ||
|
|
ef2ff50089 | ||
|
|
0b3964c827 | ||
|
|
ccf5bb9342 | ||
|
|
4303882c6a | ||
|
|
c34028d549 | ||
|
|
8e76cdcb57 | ||
|
|
5eb66106b9 | ||
|
|
0abebc1e32 | ||
|
|
552e82f44d | ||
|
|
ada40e36db | ||
|
|
480f734a06 | ||
|
|
20bee9c334 | ||
|
|
de8267f41e | ||
|
|
7bfaf07980 | ||
|
|
a42fa8e9f9 | ||
|
|
5facad683a | ||
|
|
fd1913a72e | ||
|
|
54c98b4250 | ||
|
|
5d99baac21 | ||
|
|
cb95bdf6d7 | ||
|
|
25e803abfc | ||
|
|
7c6073e4ef | ||
|
|
09bcbe8dfc | ||
|
|
e0a9c7d0bb | ||
|
|
ac27d05fd5 | ||
|
|
d6ab56252f | ||
|
|
dab178ed50 | ||
|
|
6ed50b6a75 | ||
|
|
ee559ec650 | ||
|
|
4310238418 | ||
|
|
10c47a6c38 | ||
|
|
4b8043086e | ||
|
|
fd952b88bf | ||
|
|
e262d463c5 | ||
|
|
b02bce2510 | ||
|
|
58094531c0 | ||
|
|
d937e3ca7c | ||
|
|
9c58413209 | ||
|
|
db65ff12d7 | ||
|
|
f93b819ea6 | ||
|
|
e6fea297e5 | ||
|
|
5c2a0e5634 | ||
|
|
71aa21a82d | ||
|
|
23821360c7 | ||
|
|
79f5b938f5 | ||
|
|
f8769fcc2a | ||
|
|
a3ed24c766 | ||
|
|
82727b825c | ||
|
|
16e98496af | ||
|
|
62896ce1a3 | ||
|
|
7ea5b1ecbf | ||
|
|
eecc95f8fb | ||
|
|
85808d85c4 | ||
|
|
12cc670642 | ||
|
|
13fcb55df6 | ||
|
|
a4dfd15888 | ||
|
|
7fbd326298 | ||
|
|
331d147d50 | ||
|
|
acdcdc55bc | ||
|
|
fdb0c0acb3 | ||
|
|
564bf47fb2 | ||
|
|
eca35b2371 | ||
|
|
f97d2e2644 | ||
|
|
fdd6659139 | ||
|
|
0d698fb659 | ||
|
|
c68b39dda8 | ||
|
|
cb67286bc3 | ||
|
|
a49962b8de | ||
|
|
256d5ae14f | ||
|
|
799ee8bcfa | ||
|
|
fe8a317ef9 | ||
|
|
b6d6ee45e0 | ||
|
|
0151466a28 | ||
|
|
3588875e28 | ||
|
|
e8031aec39 | ||
|
|
1cabe107e6 | ||
|
|
ba0c59744b | ||
|
|
50c702c00a | ||
|
|
ff8c2fe227 | ||
|
|
f9c78a5263 | ||
|
|
d46784a4d5 | ||
|
|
ff43d082f2 | ||
|
|
83801736d7 | ||
|
|
e16986cf71 | ||
|
|
7ad12d954c | ||
|
|
fb74fadec2 | ||
|
|
c00bdf910e | ||
|
|
50cc1c56e5 | ||
|
|
0006099758 | ||
|
|
162dcec331 | ||
|
|
3e8bd022e2 | ||
|
|
01e5936671 | ||
|
|
d85e1c70d6 | ||
|
|
f329770194 | ||
|
|
08194925cb | ||
|
|
2b7aa3e810 | ||
|
|
549417f106 | ||
|
|
f0f370cc7d | ||
|
|
b842241f8c | ||
|
|
93aa4b7440 | ||
|
|
9e2eef7818 | ||
|
|
b29b65851e | ||
|
|
a6829bff4f |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc"],
|
||||
"plugins": ["prettier", "import", "jsdoc", "jest"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true,
|
||||
"es2022": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
@@ -16,15 +16,19 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaVersion": 2022,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"eqeqeq": "error",
|
||||
"import/order": "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "off"
|
||||
"no-throw-literal": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"prefer-template": "error"
|
||||
}
|
||||
}
|
||||
|
||||
56
.gitattributes
vendored
Normal file
56
.gitattributes
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# .gitattributes snippet to force users to use same line endings for project.
|
||||
#
|
||||
# Handle line endings automatically for files detected as text
|
||||
# and leave all files detected as binary untouched.
|
||||
* text=auto
|
||||
|
||||
#
|
||||
# The above will handle all files NOT found below
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
# https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
|
||||
|
||||
|
||||
|
||||
# These files are text and should be normalized (Convert crlf => lf)
|
||||
*.php text
|
||||
*.css text
|
||||
*.scss text
|
||||
*.js text
|
||||
*.json text
|
||||
*.htm text
|
||||
*.html text
|
||||
*.xml text
|
||||
*.txt text
|
||||
*.ini text
|
||||
*.inc text
|
||||
*.pl text
|
||||
*.rb text
|
||||
*.py text
|
||||
*.scm text
|
||||
*.sql text
|
||||
.htaccess text
|
||||
*.sh text
|
||||
Dockerfile* text
|
||||
*.yml text
|
||||
*.yaml text
|
||||
*.md text
|
||||
*.markdown text
|
||||
|
||||
# These files are binary and should be left untouched
|
||||
# (binary is a macro for -text -diff)
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.flv binary
|
||||
*.fla binary
|
||||
*.swf binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.pyc binary
|
||||
25
.github/CONTRIBUTING.md
vendored
25
.github/CONTRIBUTING.md
vendored
@@ -4,23 +4,34 @@ Thanks for contributing to MagicMirror²!
|
||||
|
||||
We hold our code to standard, and these standards are documented below.
|
||||
|
||||
If you wish to run our linters, use `npm run lint` without any arguments.
|
||||
## Linters
|
||||
|
||||
We use prettier for automatic linting of all our files: `npm run lint:prettier`.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
|
||||
Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
|
||||
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
|
||||
### CSS: Run StyleLint
|
||||
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
|
||||
|
||||
To run StyleLint, use `npm run lint:style`.
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
|
||||
### Submitting Issues
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
|
||||
To run all tests, use `npm run test`.
|
||||
|
||||
The specific test commands are defined in `package.json`.
|
||||
So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
Please only submit reproducible issues.
|
||||
|
||||
@@ -32,9 +43,9 @@ When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
**Node Version**: Make sure it's version 18 or later (recommended is 20).
|
||||
|
||||
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
|
||||
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
|
||||
2
.github/FUNDING.yaml
vendored
Normal file
2
.github/FUNDING.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: MichMich
|
||||
custom: ["https://magicmirror.builders/#donate"]
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
github: MichMich
|
||||
custom: ['https://magicmirror.builders/#donate']
|
||||
@@ -1,3 +1,7 @@
|
||||
Hello and thank you for opening an issue.
|
||||
|
||||
**Please make sure that you have read the following lines before submitting your Issue:**
|
||||
|
||||
## I'm not sure if this is a bug
|
||||
|
||||
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
@@ -6,17 +10,21 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error.
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror² directory: `npm run config:check` and see if it reports an error.
|
||||
|
||||
## I found a bug in the MagicMirror installer
|
||||
## I found a bug in the MagicMirror² installer
|
||||
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror² via the installer please report it in the respective GitHub repository:
|
||||
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
|
||||
|
||||
## I found a bug in the MagicMirror Docker image
|
||||
## I found a bug in the MagicMirror² Docker image
|
||||
|
||||
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image:
|
||||
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
|
||||
If you are facing an issue or found a bug while running MagicMirror² inside a Docker container please create an issue in the corresponding repository:
|
||||
[https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
|
||||
|
||||
## I'm having troubles installing or configuring foreign modules
|
||||
|
||||
Please open an issue in the module repository or ask for help in the [forum](https://forum.magicmirror.builders/)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,9 +35,9 @@ When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
**Node Version**: Make sure it's version 18 or later (recommended is 20).
|
||||
|
||||
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
|
||||
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,25 +1,20 @@
|
||||
Hello and thank you for wanting to contribute to the MagicMirror project
|
||||
Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
|
||||
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
|
||||
|
||||
> 1) Base your pull requests against the `develop` branch.
|
||||
> 1. Base your pull requests against the `develop` branch.
|
||||
> 2. Include these infos in the description:
|
||||
>
|
||||
> - Does the pull request solve a **related** issue?
|
||||
> - If so, can you reference the issue like this `Fixes #<issue_number>`?
|
||||
> - What does the pull request accomplish? Use a list if needed.
|
||||
> - If it includes major visual changes please add screenshots.
|
||||
>
|
||||
> 2) Include these infos in the description:
|
||||
> * Does the pull request solve a **related** issue?
|
||||
> * If so, can you reference the issue like this `Fixes #<issue_number>`?
|
||||
> * What does the pull request accomplish? Use a list if needed.
|
||||
> * If it includes major visual changes please add screenshots.
|
||||
>
|
||||
>
|
||||
> 3) Please run `npm run lint:prettier` before submitting so that
|
||||
> 3. Please run `npm run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
>
|
||||
> 4) Don't forget to add an entry about your changes to
|
||||
> 4. Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
|
||||
**Note**: Sometimes the development moves very fast. It is highly
|
||||
recommended that you update your branch of `develop` before creating a
|
||||
pull request to send us your changes. This makes everyone's lives
|
||||
|
||||
10
.github/codecov.yaml
vendored
Normal file
10
.github/codecov.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
threshold: 0%
|
||||
target: 0
|
||||
25
.github/dependabot.yaml
vendored
Normal file
25
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/vendor"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fonts"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
BIN
.github/header.psd
vendored
BIN
.github/header.psd
vendored
Binary file not shown.
0
.github/stale.yml → .github/stale.yaml
vendored
0
.github/stale.yml → .github/stale.yaml
vendored
41
.github/workflows/automated-tests.yaml
vendored
Normal file
41
.github/workflows/automated-tests.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: "Run Automated Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm run install-mm:dev
|
||||
- name: "Run tests"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
touch css/custom.css
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test
|
||||
35
.github/workflows/codecov-test-suites.yaml
vendored
Normal file
35
.github/workflows/codecov-test-suites.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# This workflow runs the automated test and uploads the coverage results to codecov.io
|
||||
# For more information see: https://github.com/codecov/codecov-action
|
||||
|
||||
name: "Run Codecov Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm ci
|
||||
- name: "Run coverage"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- name: "Upload coverage results to codecov"
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
||||
24
.github/workflows/codecov-test-suites.yml
vendored
24
.github/workflows/codecov-test-suites.yml
vendored
@@ -1,24 +0,0 @@
|
||||
# This workflow runs the automated test and uploads the coverage results to codecov.io
|
||||
|
||||
name: "Run Codecov Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
npm run test:coverage
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
||||
18
.github/workflows/depsreview.yaml
vendored
Normal file
18
.github/workflows/depsreview.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# This workflow scans your pull requests for dependency changes, and will raise an error if any vulnerabilities or invalid licenses are being introduced.
|
||||
# For more information see: https://github.com/actions/dependency-review-action
|
||||
|
||||
name: "Review Dependencies"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
17
.github/workflows/enforce-changelog.yml
vendored
17
.github/workflows/enforce-changelog.yml
vendored
@@ -1,17 +0,0 @@
|
||||
# This workflow enforces the update of a changelog file on every pull request
|
||||
|
||||
name: "Enforce Changelog"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip Changelog'
|
||||
28
.github/workflows/enforce-pullrequest-rules.yaml
vendored
Normal file
28
.github/workflows/enforce-pullrequest-rules.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# This workflow enforces on every pull request:
|
||||
# - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer
|
||||
# - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files
|
||||
|
||||
name: "Enforce Pull-Request Rules"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: "Enforce changelog"
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
||||
- name: "Enforce develop branch"
|
||||
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
run: |
|
||||
echo "This PR is based against the master branch and not a release or hotfix."
|
||||
echo "Please don't do this. Switch the branch to 'develop'."
|
||||
exit 1
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
||||
32
.github/workflows/node-ci.js.yml
vendored
32
.github/workflows/node-ci.js.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: "Run Automated Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test:e2e
|
||||
npm run test:unit
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,7 +16,6 @@ vendor/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output/
|
||||
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
|
||||
7
.husky/pre-commit
Executable file
7
.husky/pre-commit
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
if command -v npm &> /dev/null; then
|
||||
npm run lint:staged
|
||||
fi
|
||||
@@ -1,5 +1,3 @@
|
||||
/config
|
||||
/coverage
|
||||
package-lock.json
|
||||
/config/**/*
|
||||
/vendor/**/*
|
||||
!/vendor/vendor.js
|
||||
.github/**/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["stylelint-prettier/recommended"],
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
|
||||
498
CHANGELOG.md
498
CHANGELOG.md
@@ -3,7 +3,446 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
|
||||
|
||||
## [2.25.0] - 2023-10-01
|
||||
|
||||
Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet.
|
||||
|
||||
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
|
||||
|
||||
> ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work!
|
||||
|
||||
### Added
|
||||
|
||||
- Added UV Index support to OpenWeatherMap
|
||||
- Added 'hideDuplicates' flag to the calendar module
|
||||
- Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed
|
||||
- Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()`
|
||||
- Added AnimateIn and animateOut in module config definition
|
||||
- Apply AnimateIn rules on the first start
|
||||
- Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105)
|
||||
- Added eventClass option for customEvents on the default calendar
|
||||
- Added AnimateCSS integration in tests suite (#3206)
|
||||
- Added npm dependabot [Reserved to developer] (#3210)
|
||||
- Added improved logging for calendar (#3110)
|
||||
|
||||
### Removed
|
||||
|
||||
- **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`)
|
||||
|
||||
### Updated
|
||||
|
||||
- Update roboto fonts to version v5
|
||||
- Update issue template
|
||||
- Update dev/dependencies incl. electron to v26
|
||||
- Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>)
|
||||
- Update engine node >=18. v16 reached it's end of life. (#3170)
|
||||
- Update typescript definition for modules
|
||||
- Cleaned up nunjuck templates
|
||||
- Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch`
|
||||
- Update the French translation according to the English file.
|
||||
- Update dependabot incl. vendor/fonts (monthly check)
|
||||
- Renew `package-lock.json` for release
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix engine check on npm install (#3135)
|
||||
- Fix undefined formatTime method in clock module (#3143)
|
||||
- Fix clientonly startup fails after async added (#3151)
|
||||
- Fix electron width/heigth when using xrandr under bullseye
|
||||
- Fix time issue with certain recurring events in calendar module
|
||||
- Fix ipWhiteList test (#3179)
|
||||
- Fix newsfeed: Convert HTML entities, codes and tag in description (#3191)
|
||||
- Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174)
|
||||
- Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call
|
||||
- Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204)
|
||||
- Fix overriding `config.js` when running tests (#3201)
|
||||
- Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast
|
||||
|
||||
## [2.24.0] - 2023-07-01
|
||||
|
||||
Thanks to: @angeldeejay, @bugsounet, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @eddiehung, @grenagit, @Hirschberger, @ismarslomic, @JakeBinney, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @oscarb, @OWL4C, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt
|
||||
|
||||
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
|
||||
|
||||
### Added
|
||||
|
||||
- Added UV Index to hourly and current Weather, with support for Openmeteo
|
||||
- Added tests for serveronly
|
||||
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
||||
- Added no-param-reassign eslint rule and fix warnings
|
||||
- updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- Added per-calendar fetchInterval
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896, second attempt ...)
|
||||
|
||||
### Updated
|
||||
|
||||
- Added support for precipitation probability with openmeteo weather-provider
|
||||
- Update electron to v25.2 and other dependencies
|
||||
- Use node v20 in github workflow (replacing v14)
|
||||
- Refactor formatTime into common util function for default modules
|
||||
- Refactor some calendar methods into own class and added tests for them
|
||||
- Split install and run commands in github actions
|
||||
- Changed `fetchInterval` of calendar in `config.js.sample` to 7 days so we not to request example calendar too frequently
|
||||
- Changed default calendar fetchInterval to one hour
|
||||
- Changed calendar url in sample config
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix envcanada hourly forecast time (#3080)
|
||||
- Fix electron not running under windows after async changes (#3083)
|
||||
- Fix style issues after eslint-plugin-jsdoc update
|
||||
- Fix don't filter out ongoing full day events (#3095)
|
||||
- Fix date not shown when clock in analog mode (#3100)
|
||||
- Fix envcanada today percentage-of-precipitation (#3106)
|
||||
- Fix updatenotification where no branch is checked out but e.g. a version tag (#3130)
|
||||
- Fix yr weather provider after changes in yr API (#3189)
|
||||
|
||||
## [2.23.0] - 2023-04-04
|
||||
|
||||
Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.
|
||||
|
||||
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
|
||||
|
||||
### Added
|
||||
|
||||
- Added increments for hourly forecasts in weather module (#2996)
|
||||
- Added tests for hourly weather forecast
|
||||
- Added possibility to ignore MagicMirror repo in updatenotification module
|
||||
- Added Pirate Weather as new weather-provider (#3005)
|
||||
- Added possibility to use your own templates in Alert module
|
||||
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
|
||||
- Added possibility to use environment variables in `config.js` (#1756)
|
||||
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
|
||||
- Added thai language to alert module
|
||||
- Added option `sendNotifications` in clock module (#3056)
|
||||
- Added tests for some weather utils
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed darksky weather-provider
|
||||
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)
|
||||
|
||||
### Updated
|
||||
|
||||
- Use develop as target branch for dependabot
|
||||
- Update issue template, contributing doc and sample config
|
||||
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
|
||||
- This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
|
||||
- Update tests for weather and calendar module
|
||||
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
|
||||
- Update dates in Calendar widgets every minute
|
||||
- Cleanup jest coverage for patches
|
||||
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
|
||||
- Update Eslint config, add new rule and handle issue
|
||||
- Convert lots of callbacks to async/await
|
||||
- Revise require imports (#3071 and #3072)
|
||||
- Use `config.js-old` instead of file with timestamp suffix when backing up config with a `config.template` in use (#3104)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix wrong day labels in envcanada forecast (#2987)
|
||||
- Fix for missing default class name prefix for customEvents in calendar
|
||||
- Fix electron flashing white screen on startup (#1919)
|
||||
- Fix weathergov provider hourly forecast (#3008)
|
||||
- Fix message display with HTML code into alert module (#2828)
|
||||
- Fix typo in french translation
|
||||
- Yr wind direction is no longer inverted
|
||||
- Fix async node_helper stopping electron start (#2487)
|
||||
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
|
||||
- Fix precipitation css styles and rounding value
|
||||
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
|
||||
- Fix empty news feed stopping the reload forever
|
||||
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
|
||||
- Lint: Use template literals instead of string concatenation
|
||||
- Fix default alert module to render HTML for title and message
|
||||
- Fix Open-Meteo wind speed units
|
||||
|
||||
## [2.22.0] - 2023-01-01
|
||||
|
||||
Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom.
|
||||
|
||||
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you!
|
||||
|
||||
### Added
|
||||
|
||||
- Added new calendar options for colored entries and improved styling (#3033)
|
||||
- Added test for remoteFile option in compliments module
|
||||
- Added hourlyWeather functionality to Weather.gov weather-provider
|
||||
- Added css class names "today" and "tomorrow" for default calendar
|
||||
- Added Collaboration.md
|
||||
- Added new github action for dependency review (#2862)
|
||||
- Added a WeatherProvider for Open-Meteo
|
||||
- Added Yr as a weather-provider
|
||||
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
||||
- Added thai language
|
||||
- Added workflow rule to make sure PRs are based against develop
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed usage of internal fetch function of node until it is more stable
|
||||
- Removed weatherEndpoint definition from weathergov.js (not used)
|
||||
|
||||
### Updated
|
||||
|
||||
- Cleaned up test directory (#2937) and jest config (#2959)
|
||||
- Wait for all modules to start before declaring the system ready (#2487)
|
||||
- Updated e2e tests (moved `done()` in helper functions) and use es6 syntax in all tests
|
||||
- Updated da translation
|
||||
- Rework weather module
|
||||
- Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955)
|
||||
- Use fetch instead of XMLHttpRequest in weather-provider (#2935)
|
||||
- Reworked how weather-providers handle units (#2849)
|
||||
- Use unix() method for parsing times, fix suntimes on the way (#2950)
|
||||
- Refactor conversion functions into utils class (#2958)
|
||||
- The `cors`-method in `server.js` now supports sending and receiving HTTP headers
|
||||
- Replace `…` by `…`
|
||||
- Cleanup compliments module
|
||||
- Updated dependencies including electron to v22 (#2903)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correctly show apparent temperature in SMHI weather-provider
|
||||
- Ensure updatenotification module isn't shown when local is _ahead_ of remote
|
||||
- Handle node_helper errors during startup (#2944)
|
||||
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
|
||||
- Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840)
|
||||
- Tests not waiting for the application to start and stop before starting the next test
|
||||
- Fix electron tests failing sometimes in github workflow
|
||||
- Fixed gap in clock module when displayed on the left side with displayType=digital
|
||||
- Fixed playwright issue by upgrading to v1.29.1 (#2969)
|
||||
|
||||
## [2.21.0] - 2022-10-01
|
||||
|
||||
Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle, @MikeBishop, @rejas, @sdetweil, @SkySails and @veeck
|
||||
|
||||
### Added
|
||||
|
||||
- Added possibility to fetch calendars through socket notifications.
|
||||
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
|
||||
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
|
||||
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather-provider.
|
||||
- Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed old and deprecated weather modules `currentweather` and `weatherforecast`.
|
||||
- Removed `DAYAFTERTOMORROW` from English.
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated dependencies.
|
||||
- Updated jsdoc.
|
||||
- Updated font tree to use variables consistently.
|
||||
- Removed deprecated Docker Repository from issue template.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Broadcast all calendar events while still honoring global and per-calendar maximumEntries.
|
||||
- Respect rss ttl provided by newsfeed (#2883).
|
||||
- Fix multi day calendar events always presented as "(1/X)" instead of the amount of days the event has progressed.
|
||||
- Fix weatherbit provider to use type config value instead of endpoint.
|
||||
- Fix calendar events which DO NOT specify rrule byday adjusted incorrectly (#2885).
|
||||
- Fix e2e tests not failing on errors (#2911).
|
||||
|
||||
## [2.20.0] - 2022-07-02
|
||||
|
||||
Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @KristjanESPERANTO, @nathannaveen, @naveensrinivasan, @rejas, @rohitdharavath and @sdetweil.
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother website (solves #2847).
|
||||
- Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192).
|
||||
- Added the notification emitting from the weather module on information updated.
|
||||
- Use recommended file extension for YAML files (#2864).
|
||||
|
||||
### Updated
|
||||
|
||||
- Use latest node 18 when running tests on github actions.
|
||||
- Updated `electron` to v19 and other dependencies.
|
||||
- Use internal fetch function of node instead external `node-fetch` library if used node version >= `v18`.
|
||||
- Include duplicate events in broadcasts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix problems with non latin fonds caused by updating to fontsource (fixes #2835).
|
||||
|
||||
## [2.19.0] - 2022-04-01
|
||||
|
||||
Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd3rf, @khassel, @kolbyjack, @krekos, @KristjanESPERANTO, @Nerfzooka, @oraclesean, @oscarb, @philnagel, @rejas, @sdetweil, @shin10, @SiderealArt and @Tom-Hirschberger.
|
||||
|
||||
### Added
|
||||
|
||||
- Added a config option under the weather module, `absoluteDates`, providing an option to format weather forecast date output with either absolute or relative dates.
|
||||
- Added test for new weather forecast `absoluteDates` property.
|
||||
- The modules get a class hidden added/removed if they get hidden/shown which will also toggle pointer-events.
|
||||
- Added new config option `showTitleAsUrl` to newsfeed module. If set, the displayed title is a link to the article which is useful when running in a browser and you want to read this article.
|
||||
- Added internal cors proxy to get weather-providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
|
||||
- Added a WeatherProvider for Weatherflow.
|
||||
- Added new env var `ELECTRON_DISABLE_GPU` which disable gpu under electron if set (fixes #2831).
|
||||
- Added missing Czech translations.
|
||||
|
||||
### Updated
|
||||
|
||||
- Deprecated roboto fonts package `roboto-fontface-bower` replaced with `fontsource`.
|
||||
- Updated `electron` to v17, `helmet` to v5 (use defaults of v4) and other dependencies
|
||||
- Updated Font Awesome css class to new default style (fixes #2768)
|
||||
- Replaced deprecated modules `currentweather` and `weatherforecast` with dummy modules only displaying that they have to be replaced.
|
||||
- Include all calendar events from the configured date range when broadcasting.
|
||||
- Updated Danish and German translation.
|
||||
- Updated `node-ical` to v0.15 and added `luxon` as dependency for not breaking the "no-optional" install (see #2718 and #2824).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved and speedup e2e tests, artificial wait after mm start removed.
|
||||
- Improved husky setup not blocking `git commit` if `husky` or `npm` is not installed.
|
||||
- Using a consistent spelling of MagicMirror².
|
||||
- Fix minor console output issue for loading translations (#2814).
|
||||
- Don't adjust startDate for full day events if endDate is in the past.
|
||||
- Fix windspeed conversion error in openweathermap provider. (#2812)
|
||||
- Fix conflicting parms turning off showEnd for full day events. (#2629)
|
||||
- Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868)
|
||||
|
||||
## [2.18.0] - 2022-01-01
|
||||
|
||||
Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @jupadin, @khassel, @kolbyjack, @KristjanESPERANTO, @MariusVaice, @rejas, @rico24 and @sdetweil.
|
||||
|
||||
### Added
|
||||
|
||||
- Added test for calendar recurring event with checks the correct date displayed (related to #2752).
|
||||
|
||||
### Updated
|
||||
|
||||
- ESLint version supports now ECMAScript 2018.
|
||||
- Cleaned up `updatenotification` module and switched to nunjuck template.
|
||||
- Moved calendar tests from category `electron` to `e2e`.
|
||||
- Updated missed translations for Korean language (ko.json).
|
||||
- Updated missed translations for Dutch language (nl.json).
|
||||
- Cleaned up `alert` module and switched to nunjuck template.
|
||||
- Moved weather tests from category `electron` to `e2e`.
|
||||
- Updated github actions.
|
||||
- Replace spectron with playwright, update dependencies including electron update to v16.
|
||||
- Added lithuanian language to translations.js.
|
||||
- Show info message if newsfeed is empty (fixes #2731).
|
||||
- Added dangerouslyDisableAutoEscaping config option for newsfeed templates (fixes #2712).
|
||||
- Added missing shebang to `installers/mm.sh`.
|
||||
- Node versions in templates and github workflows.
|
||||
- Updated translations for Traditional Chinese (Taiwan) (zh-tw.json).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
|
||||
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
|
||||
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||
- Fixed e2e tests by increasing testTimeout.
|
||||
- Revert node-ical update due to missing luxon package.
|
||||
- Fixed User-Agent-Header for newsfeed and calendar module (#2729).
|
||||
- Replace broken shields in Readme and use https for links.
|
||||
- Fixed electron tests with retry.
|
||||
- Fixed Calendar recurring cross timezone error (add/subtract a day, not just offset hours) (#2632).
|
||||
- Fixed Calendar showEnd and Full Date overlay (#2629).
|
||||
- Fixed regression on #2632, #2752.
|
||||
- Broadcast custom symbols in CALENDAR_EVENTS.
|
||||
|
||||
## [2.17.1] - 2021-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error when accessing letsencrypt certificates
|
||||
- Fixed Calendar module enhancement: displaying full events without time (#2424)
|
||||
|
||||
## [2.17.0] - 2021-10-01
|
||||
|
||||
Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas.
|
||||
|
||||
### Added
|
||||
|
||||
- Added showTime parameter to clock module for enabling/disabling time display in analog clock.
|
||||
- Added custom electron switches from user config (`config.electronSwitches`).
|
||||
- Added unit tests for updatenotification module.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump electron to v13 (and spectron to v15) and update other dependencies in package.json.
|
||||
- Refactor test configs, use default test config for all tests.
|
||||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
- Run prettier over all relevant files.
|
||||
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
|
||||
- Updated dependencies in package.json.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix undefined error with ignoreToday option in weather module (#2620).
|
||||
- Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632).
|
||||
- Fix black cursor on startup when using electron.
|
||||
- Fix update notification not working for own repository (#2644).
|
||||
|
||||
## [2.16.0] - 2021-07-01
|
||||
|
||||
Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed.
|
||||
|
||||
### Added
|
||||
|
||||
- Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP".
|
||||
- Added German translation for "PRECIP".
|
||||
- Added Dutch translation for "WEEK", "PRECIP", "MODULE_CONFIG_CHANGED" and "MODULE_CONFIG_ERROR".
|
||||
- Added first test for Alert module.
|
||||
- Added support for `dateFormat` when not using `timeFormat: "absolute"`.
|
||||
- Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file.
|
||||
- Added custom-properties for gaps around body and between modules.
|
||||
- Added test case for recurring calendar events.
|
||||
- Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only).
|
||||
- Added list view for newsfeed module.
|
||||
- Added dev dependency jest, switching from mocha to jest.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed).
|
||||
- Use codecov in informational mode.
|
||||
- Refactor code into es6 where possible (e.g. var -> let/const).
|
||||
- Use node v16 in github workflow (replacing node v10).
|
||||
- Moved some files into better suited directories.
|
||||
- Updated dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`.
|
||||
- Updated dependencies in package.json and migrate husky to v6, fix husky setup in prod environment.
|
||||
- Cleaned up error handling in newsfeed and calendar modules for real.
|
||||
- Updated default WEATHER module such that a provider can optionally set a custom unit-of-measure for precipitation (`weatherObject.precipitationUnits`).
|
||||
- Updated documentation.
|
||||
- Updated jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests.
|
||||
- Updated dependencies in package.json.
|
||||
|
||||
### Removed
|
||||
|
||||
- Switching from mocha to jest so removed following dev dependencies: chai, chai-as-promised, mocha, mocha-each, mocha-logger.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix calendar start function logging inconsistency.
|
||||
- Fix updatenotification start function logging inconsistency.
|
||||
- Checks and applies the showDescription setting for the newsfeed module again.
|
||||
- Fix issue with openweathermap not showing current or forecast info when using onecall API.
|
||||
- Fix tests in weather module and add one for decimalPoint in forecast.
|
||||
- Fix decimalSymbol in the forecast part of the new weather module (#2530).
|
||||
- Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`.
|
||||
- Fix alert not recognizing multiple alerts (#2522).
|
||||
- Fix fetch option httpsAgent to agent in calendar module (#466).
|
||||
- Fix module updatenotification which did not work for repos with many refs (#1907).
|
||||
- Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config").
|
||||
- Fix calendar debug check.
|
||||
- Really run prettier over all files.
|
||||
- Fix logger.js after jest changes, use --forceExit running jest.
|
||||
- Workaround for dev_console test using getWindowCount.
|
||||
|
||||
## [2.15.0] - 2021-04-01
|
||||
|
||||
@@ -35,7 +474,7 @@ Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @
|
||||
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
|
||||
- Converted newsfeed module to use templates.
|
||||
- Updated documentation and help screen about invalid config files.
|
||||
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Moving weather-provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Bump electron to v11 and enable contextIsolation.
|
||||
- Don't update the DOM when a module is not displayed.
|
||||
- Cleaned up jsdoc and tests.
|
||||
@@ -93,10 +532,10 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
|
||||
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||
- Update dependencies to latest versions.
|
||||
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Update lithuanian translation.
|
||||
- Update config sample.
|
||||
- Updated dependencies to latest versions.
|
||||
- Updated dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Updated lithuanian translation.
|
||||
- Updated config sample.
|
||||
- Highlight required version mismatch.
|
||||
- No select Text for TouchScreen use.
|
||||
- Corrected logic for timeFormat "relative" and "absolute".
|
||||
@@ -117,19 +556,19 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix windspeed conversion error in ukmetoffice weather-provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other than English. (#2221)
|
||||
- Update node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Updated node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Updated node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
- Updated node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Updated simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
|
||||
## [2.13.0] - 2020-10-01
|
||||
|
||||
@@ -140,8 +579,8 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
|
||||
### Added
|
||||
|
||||
- `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
MagicMirror² from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror² and/or MagicMirror² modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Added lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
@@ -214,7 +653,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
🚨 READ THIS BEFORE UPDATING 🚨
|
||||
|
||||
In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror folder. In other words: update at your own risk.
|
||||
In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror² folder. In other words: update at your own risk.
|
||||
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860).
|
||||
|
||||
@@ -346,7 +785,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Use Feels Like temp from feed if present
|
||||
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
@@ -354,9 +793,10 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
|
||||
- English translation for "Feels" to "Feels like"
|
||||
- Fixed the example calendar url in `config.js.sample`
|
||||
- Update `ical.js` to solve various calendar issues.
|
||||
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Updated `ical.js` to solve various calendar issues.
|
||||
- Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Updated weather-provider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -376,7 +816,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- use current username vs hardcoded 'pi' to support non-pi install
|
||||
- check for npm installed. node install doesn't do npm anymore
|
||||
- check for mac as part of PM2 install, add install option string
|
||||
- update pm2 config with current username instead of hard coded 'pi'
|
||||
- Updated pm2 config with current username instead of hard coded 'pi'
|
||||
- check for screen saver config, "/etc/xdg/lxsession", bypass if not setup
|
||||
|
||||
## [2.7.1] - 2019-04-02
|
||||
@@ -414,7 +854,7 @@ Fixed `package.json` version number.
|
||||
- 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).
|
||||
- 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)
|
||||
@@ -438,7 +878,7 @@ Fixed `package.json` version number.
|
||||
- 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
|
||||
- Added weather.gov as a new weather-provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
|
||||
@@ -526,7 +966,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
## [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).
|
||||
⚠️ **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`
|
||||
|
||||
@@ -584,7 +1024,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add types for module.
|
||||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow scrolling 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
|
||||
- Changed 'compliments.js' - Updated 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
|
||||
@@ -596,7 +1036,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
- 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.
|
||||
- Updated 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.
|
||||
|
||||
@@ -767,7 +1207,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add use pm2 for manager process into Installer RaspberryPi script.
|
||||
- Russian Translation.
|
||||
- Afrikaans Translation.
|
||||
- Add postinstall script to notify user that MagicMirror installed successfully despite warnings from NPM.
|
||||
- Add postinstall script to notify user that MagicMirror² installed successfully despite warnings from NPM.
|
||||
- Init tests using mocha.
|
||||
- Option to use RegExp in Calendar's titleReplace.
|
||||
- Hungarian Translation.
|
||||
@@ -803,7 +1243,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update .gitignore to not ignore default modules folder.
|
||||
- Updated .gitignore to not ignore default modules folder.
|
||||
- Remove white flash on boot up.
|
||||
- Added `update` in Raspberry Pi installation script.
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611))
|
||||
@@ -830,7 +1270,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add VSCode IntelliSense support.
|
||||
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information.
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror² version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the 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
|
||||
@@ -888,8 +1328,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Updated
|
||||
|
||||
- Force fullscreen when kioskmode is active.
|
||||
- Update the .github templates and information with more modern information.
|
||||
- Update the Gruntfile with a more functional StyleLint implementation.
|
||||
- Updated the .github templates and information with more modern information.
|
||||
- Updated the Gruntfile with a more functional StyleLint implementation.
|
||||
|
||||
## [2.0.4] - 2016-08-07
|
||||
|
||||
@@ -977,6 +1417,6 @@ It includes (but is not limited to) the following features:
|
||||
|
||||
## [1.0.0] - 2014-02-16
|
||||
|
||||
### Initial release of MagicMirror.
|
||||
### Initial release of MagicMirror
|
||||
|
||||
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
|
||||
|
||||
18
Collaboration.md
Normal file
18
Collaboration.md
Normal file
@@ -0,0 +1,18 @@
|
||||
This document describes how collaborators of this repository should work together.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- never merge your own PR's
|
||||
- never merge without someone having approved (approving and merging from same person is allowed)
|
||||
- wait for all approvals requested (or the author decides something different in the comments)
|
||||
- never merge to `master`, except for releases (because of update notification)
|
||||
- merges to master should be tagged with the "mastermerge" label so that the test runs through
|
||||
|
||||
## Issues
|
||||
|
||||
- "real" Issues are closed if the problem is solved and the fix is released
|
||||
- unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord
|
||||
|
||||
## Releases
|
||||
|
||||
- are done by @MichMich only
|
||||
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
Copyright © 2016-2022 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
||||
21
README.md
21
README.md
@@ -1,13 +1,17 @@
|
||||

|
||||
|
||||
<p style="text-align: center">
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
|
||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg" /></a>
|
||||
<a href="https://choosealicense.com/licenses/mit">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror">
|
||||
<img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/>
|
||||
</a>
|
||||
<a href="https://github.com/MichMich/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
|
||||
</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](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -23,6 +27,7 @@ For the full documentation including **[installation instructions](https://docs.
|
||||
- Website: [https://magicmirror.builders](https://magicmirror.builders)
|
||||
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
|
||||
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
|
||||
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
|
||||
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
|
||||
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
|
||||
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
|
||||
@@ -35,7 +40,7 @@ Contributions of all kinds are welcome, not only in the form of code but also wi
|
||||
- documentation
|
||||
- translations
|
||||
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/about/contributing.html](https://docs.magicmirror.builders/about/contributing.html)
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Use separate scope to prevent global scope pollution
|
||||
(function () {
|
||||
var config = {};
|
||||
const config = {};
|
||||
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
@@ -11,14 +11,13 @@
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
var index = process.argv.indexOf(`--${key}`);
|
||||
var value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
const index = process.argv.indexOf(`--${key}`);
|
||||
const value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
return value !== undefined ? String(value) : defaultValue;
|
||||
}
|
||||
|
||||
@@ -33,7 +32,6 @@
|
||||
|
||||
/**
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
@@ -43,7 +41,7 @@
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const request = lib.get(url, (response) => {
|
||||
var configData = "";
|
||||
let configData = "";
|
||||
|
||||
// Gather incoming data
|
||||
response.on("data", function (chunk) {
|
||||
@@ -63,7 +61,6 @@
|
||||
|
||||
/**
|
||||
* Print a message to the console in case of errors
|
||||
*
|
||||
* @param {string} message error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
@@ -79,15 +76,16 @@
|
||||
getServerAddress();
|
||||
|
||||
(config.address && config.port) || fail();
|
||||
var prefix = config.tls ? "https://" : "http://";
|
||||
const prefix = config.tls ? "https://" : "http://";
|
||||
|
||||
// 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(`${prefix}${config.address}:${config.port}/config/`)
|
||||
.then(function (configReturn) {
|
||||
// Pass along the server config via an environment variable
|
||||
var env = Object.create(process.env);
|
||||
var options = { env: env };
|
||||
const env = Object.create(process.env);
|
||||
env.clientonly = true; // set to pass to electron.js
|
||||
const options = { env: env };
|
||||
configReturn.address = config.address;
|
||||
configReturn.port = config.port;
|
||||
configReturn.tls = config.tls;
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
/* Magic Mirror Config Sample
|
||||
/* MagicMirror² Config Sample
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* See https://github.com/MichMich/MagicMirror#configuration
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html
|
||||
* and https://docs.magicmirror.builders/modules/configuration.html
|
||||
*
|
||||
* You can use environment variables using a `config.js.template` file instead of `config.js`
|
||||
* which will be converted to `config.js` while starting. For more information
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables
|
||||
*/
|
||||
|
||||
var config = {
|
||||
let config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||
// - another specific IPv4/6 to listen on a specific interface
|
||||
// - "0.0.0.0", "::" to listen on any interface
|
||||
// Default, when address config is left out or empty, is "localhost"
|
||||
port: 8080,
|
||||
basePath: "/", // The URL path where MagicMirror is hosted. If you are using a Reverse proxy
|
||||
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
|
||||
// you must set the sub path here. basePath must end with a /
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
||||
// or add a specific IPv4 of 192.168.1.5 :
|
||||
@@ -32,11 +35,6 @@ var config = {
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
// serverOnly: true/false/"local" ,
|
||||
// local for armv6l processors, default
|
||||
// starts serveronly and then starts chrome browser
|
||||
// false, default for all NON-armv6l devices
|
||||
// true, force serveronly mode, because you want to.. no UI on this device
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -57,8 +55,10 @@ var config = {
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
fetchInterval: 7 * 24 * 60 * 60 * 1000,
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
|
||||
url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
31
css/custom.css.sample
Normal file
31
css/custom.css.sample
Normal file
@@ -0,0 +1,31 @@
|
||||
/* MagicMirror² Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */
|
||||
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: black;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
117
css/main.css
117
css/main.css
@@ -1,8 +1,29 @@
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: #000;
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
--font-size: 20px;
|
||||
--font-size-xsmall: 0.75rem;
|
||||
--font-size-small: 1rem;
|
||||
--font-size-medium: 1.5rem;
|
||||
--font-size-large: 3.25rem;
|
||||
--font-size-xlarge: 3.75rem;
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
||||
html {
|
||||
cursor: none;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
background: var(--color-background);
|
||||
user-select: none;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -10,16 +31,15 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 60px;
|
||||
margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left);
|
||||
position: absolute;
|
||||
height: calc(100% - 120px);
|
||||
width: calc(100% - 120px);
|
||||
background: #000;
|
||||
color: #aaa;
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom));
|
||||
width: calc(100% - var(--gap-body-right) - var(--gap-body-left));
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 2em;
|
||||
line-height: 1.5em;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@@ -28,60 +48,60 @@ body {
|
||||
*/
|
||||
|
||||
.dimmed {
|
||||
color: #666;
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: #fff;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-size: var(--font-size-xsmall);
|
||||
line-height: 1.275;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 20px;
|
||||
line-height: 25px;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 30px;
|
||||
line-height: 35px;
|
||||
font-size: var(--font-size-medium);
|
||||
line-height: 1.225;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 65px;
|
||||
line-height: 65px;
|
||||
font-size: var(--font-size-large);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 75px;
|
||||
line-height: 75px;
|
||||
font-size: var(--font-size-xlarge);
|
||||
line-height: 1;
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
|
||||
.thin {
|
||||
font-family: Roboto, sans-serif;
|
||||
font-family: var(--font-secondary), sans-serif;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.light {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -95,14 +115,14 @@ body {
|
||||
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #666;
|
||||
border-bottom: 1px solid var(--color-text-dimmed);
|
||||
line-height: 15px;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
sup {
|
||||
@@ -115,11 +135,19 @@ sup {
|
||||
*/
|
||||
|
||||
.module {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: var(--gap-modules);
|
||||
}
|
||||
|
||||
.module.hidden {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.module:not(.hidden) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.region.bottom .module {
|
||||
margin-top: 30px;
|
||||
margin-top: var(--gap-modules);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -143,17 +171,10 @@ sup {
|
||||
|
||||
.region.fullscreen {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
left: -60px;
|
||||
right: -60px;
|
||||
bottom: -60px;
|
||||
inset: calc(-1 * var(--gap-body-top)) calc(-1 * var(--gap-body-right)) calc(-1 * var(--gap-body-bottom)) calc(-1 * var(--gap-body-left));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.region.fullscreen * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.region.right {
|
||||
right: 0;
|
||||
text-align: right;
|
||||
@@ -163,18 +184,6 @@ sup {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.region.top .container {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.region.bottom .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.region.top .container:empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.region.top.center,
|
||||
.region.bottom.center {
|
||||
left: 50%;
|
||||
@@ -191,10 +200,6 @@ sup {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.region.bottom .container:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.region.bottom.right,
|
||||
.region.bottom.center,
|
||||
.region.bottom.left {
|
||||
|
||||
35
fonts/package-lock.json
generated
35
fonts/package-lock.json
generated
@@ -1,12 +1,37 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"roboto-fontface": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
|
||||
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
|
||||
"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
|
||||
},
|
||||
"node_modules/@fontsource/roboto-condensed": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz",
|
||||
"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
|
||||
"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
|
||||
},
|
||||
"@fontsource/roboto-condensed": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.8.tgz",
|
||||
"integrity": "sha512-xAXYY+ys24OZ/eOfXJZILPu2xOB7c0ZruM4cd4TSzX3WGj4dZbXYwCEowLldKbZye6LTqiltpFLP/g/Ne0qGLg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror Core.",
|
||||
"description": "Package for fonts use by MagicMirror² Core.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
@@ -10,6 +10,7 @@
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
665
fonts/roboto.css
665
fonts/roboto.css
@@ -1,58 +1,671 @@
|
||||
/* roboto-cyrillic-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-100-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Roboto"), local("Roboto-Regular"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
25
index.html
25
index.html
@@ -1,22 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
<meta name="google" content="notranslate" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
|
||||
<link rel="stylesheet" type="text/css" href="vendor/node_modules/animate.css/animate.min.css" />
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
|
||||
<script type="text/javascript">
|
||||
var version = "#VERSION#";
|
||||
window.mmVersion = "#VERSION#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -29,7 +30,9 @@
|
||||
</div>
|
||||
<div class="region upper third"><div class="container"></div></div>
|
||||
<div class="region middle center"><div class="container"></div></div>
|
||||
<div class="region lower third"><div class="container"><br/></div></div>
|
||||
<div class="region lower third">
|
||||
<div class="container"><br /></div>
|
||||
</div>
|
||||
<div class="region bottom bar">
|
||||
<div class="container"></div>
|
||||
<div class="region bottom left"><div class="container"></div></div>
|
||||
@@ -43,6 +46,7 @@
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
|
||||
<script type="text/javascript" src="modules/default/utils.js"></script>
|
||||
<script type="text/javascript" src="js/logger.js"></script>
|
||||
<script type="text/javascript" src="translations/translations.js"></script>
|
||||
<script type="text/javascript" src="js/translator.js"></script>
|
||||
@@ -50,6 +54,7 @@
|
||||
<script type="text/javascript" src="js/module.js"></script>
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
||||
|
||||
33
jest.config.js
Normal file
33
jest.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = async () => {
|
||||
return {
|
||||
verbose: true,
|
||||
testTimeout: 20000,
|
||||
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
||||
projects: [
|
||||
{
|
||||
displayName: "unit",
|
||||
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
||||
moduleNameMapper: {
|
||||
logger: "<rootDir>/js/logger.js"
|
||||
},
|
||||
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "electron",
|
||||
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||
}
|
||||
],
|
||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],
|
||||
coverageReporters: ["lcov", "text"],
|
||||
coverageProvider: "v8"
|
||||
};
|
||||
};
|
||||
164
js/animateCSS.js
Normal file
164
js/animateCSS.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/* MagicMirror²
|
||||
* AnimateCSS System from https://animate.style/
|
||||
* by @bugsounet
|
||||
* for Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* enumeration of animations in Array **/
|
||||
const AnimateCSSIn = [
|
||||
// Attention seekers
|
||||
"bounce",
|
||||
"flash",
|
||||
"pulse",
|
||||
"rubberBand",
|
||||
"shakeX",
|
||||
"shakeY",
|
||||
"headShake",
|
||||
"swing",
|
||||
"tada",
|
||||
"wobble",
|
||||
"jello",
|
||||
"heartBeat",
|
||||
// Back entrances
|
||||
"backInDown",
|
||||
"backInLeft",
|
||||
"backInRight",
|
||||
"backInUp",
|
||||
// Bouncing entrances
|
||||
"bounceIn",
|
||||
"bounceInDown",
|
||||
"bounceInLeft",
|
||||
"bounceInRight",
|
||||
"bounceInUp",
|
||||
// Fading entrances
|
||||
"fadeIn",
|
||||
"fadeInDown",
|
||||
"fadeInDownBig",
|
||||
"fadeInLeft",
|
||||
"fadeInLeftBig",
|
||||
"fadeInRight",
|
||||
"fadeInRightBig",
|
||||
"fadeInUp",
|
||||
"fadeInUpBig",
|
||||
"fadeInTopLeft",
|
||||
"fadeInTopRight",
|
||||
"fadeInBottomLeft",
|
||||
"fadeInBottomRight",
|
||||
// Flippers
|
||||
"flip",
|
||||
"flipInX",
|
||||
"flipInY",
|
||||
// Lightspeed
|
||||
"lightSpeedInRight",
|
||||
"lightSpeedInLeft",
|
||||
// Rotating entrances
|
||||
"rotateIn",
|
||||
"rotateInDownLeft",
|
||||
"rotateInDownRight",
|
||||
"rotateInUpLeft",
|
||||
"rotateInUpRight",
|
||||
// Specials
|
||||
"jackInTheBox",
|
||||
"rollIn",
|
||||
// Zooming entrances
|
||||
"zoomIn",
|
||||
"zoomInDown",
|
||||
"zoomInLeft",
|
||||
"zoomInRight",
|
||||
"zoomInUp",
|
||||
// Sliding entrances
|
||||
"slideInDown",
|
||||
"slideInLeft",
|
||||
"slideInRight",
|
||||
"slideInUp"
|
||||
];
|
||||
|
||||
const AnimateCSSOut = [
|
||||
// Back exits
|
||||
"backOutDown",
|
||||
"backOutLeft",
|
||||
"backOutRight",
|
||||
"backOutUp",
|
||||
// Bouncing exits
|
||||
"bounceOut",
|
||||
"bounceOutDown",
|
||||
"bounceOutLeft",
|
||||
"bounceOutRight",
|
||||
"bounceOutUp",
|
||||
// Fading exits
|
||||
"fadeOut",
|
||||
"fadeOutDown",
|
||||
"fadeOutDownBig",
|
||||
"fadeOutLeft",
|
||||
"fadeOutLeftBig",
|
||||
"fadeOutRight",
|
||||
"fadeOutRightBig",
|
||||
"fadeOutUp",
|
||||
"fadeOutUpBig",
|
||||
"fadeOutTopLeft",
|
||||
"fadeOutTopRight",
|
||||
"fadeOutBottomRight",
|
||||
"fadeOutBottomLeft",
|
||||
// Flippers
|
||||
"flipOutX",
|
||||
"flipOutY",
|
||||
// Lightspeed
|
||||
"lightSpeedOutRight",
|
||||
"lightSpeedOutLeft",
|
||||
// Rotating exits
|
||||
"rotateOut",
|
||||
"rotateOutDownLeft",
|
||||
"rotateOutDownRight",
|
||||
"rotateOutUpLeft",
|
||||
"rotateOutUpRight",
|
||||
// Specials
|
||||
"hinge",
|
||||
"rollOut",
|
||||
// Zooming exits
|
||||
"zoomOut",
|
||||
"zoomOutDown",
|
||||
"zoomOutLeft",
|
||||
"zoomOutRight",
|
||||
"zoomOutUp",
|
||||
// Sliding exits
|
||||
"slideOutDown",
|
||||
"slideOutLeft",
|
||||
"slideOutRight",
|
||||
"slideOutUp"
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an animation with Animate CSS
|
||||
* @param {string} [element] div element to animate.
|
||||
* @param {string} [animation] animation name.
|
||||
* @param {number} [animationTime] animation duration.
|
||||
*/
|
||||
function addAnimateCSS(element, animation, animationTime) {
|
||||
const animationName = `animate__${animation}`;
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn(`addAnimateCSS: node not found for`, element);
|
||||
return;
|
||||
}
|
||||
node.style.setProperty("--animate-duration", `${animationTime}s`);
|
||||
node.classList.add("animate__animated", animationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an animation with Animate CSS
|
||||
* @param {string} [element] div element to animate.
|
||||
* @param {string} [animation] animation name.
|
||||
*/
|
||||
function removeAnimateCSS(element, animation) {
|
||||
const animationName = `animate__${animation}`;
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn(`removeAnimateCSS: node not found for`, element);
|
||||
return;
|
||||
}
|
||||
node.classList.remove("animate__animated", animationName);
|
||||
node.style.removeProperty("--animate-duration");
|
||||
}
|
||||
203
js/app.js
203
js/app.js
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -10,6 +10,7 @@ require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const envsub = require("envsub");
|
||||
const Log = require("logger");
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
@@ -17,7 +18,7 @@ const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`)
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
@@ -37,38 +38,85 @@ if (process.env.MM_PORT) {
|
||||
process.on("uncaughtException", function (err) {
|
||||
Log.error("Whoops! There was an uncaught exception...");
|
||||
Log.error(err);
|
||||
Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
});
|
||||
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
function App() {
|
||||
let nodeHelpers = [];
|
||||
let httpServer;
|
||||
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* callback with the found config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after loading the config
|
||||
* Loads the config file. Combines it with the defaults and returns the config
|
||||
* @async
|
||||
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
|
||||
*/
|
||||
function loadConfig(callback) {
|
||||
async function loadConfig() {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
let templateFile = `${configFilename}.template`;
|
||||
|
||||
// check if templateFile exists
|
||||
try {
|
||||
fs.accessSync(templateFile, fs.F_OK);
|
||||
} catch (err) {
|
||||
templateFile = null;
|
||||
Log.debug("config template file not exists, no envsubst");
|
||||
}
|
||||
|
||||
if (templateFile) {
|
||||
// save current config.js
|
||||
try {
|
||||
if (fs.existsSync(configFilename)) {
|
||||
fs.copyFileSync(configFilename, `${configFilename}-old`);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
|
||||
}
|
||||
|
||||
// check if config.env exists
|
||||
const envFiles = [];
|
||||
const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`;
|
||||
try {
|
||||
if (fs.existsSync(configEnvFile)) {
|
||||
envFiles.push(configEnvFile);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
|
||||
}
|
||||
|
||||
let options = {
|
||||
all: true,
|
||||
diff: false,
|
||||
envFiles: envFiles,
|
||||
protect: false,
|
||||
syntax: "default",
|
||||
system: true
|
||||
};
|
||||
|
||||
// envsubst variables in templateFile and create new config.js
|
||||
// naming for envsub must be templateFile and outputFile
|
||||
const outputFile = configFilename;
|
||||
try {
|
||||
await envsub({ templateFile, outputFile, options });
|
||||
} catch (err) {
|
||||
Log.error(`Could not envsubst variables: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
const c = require(configFilename);
|
||||
checkDeprecatedOptions(c);
|
||||
const config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
return Object.assign(defaults, c);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
@@ -77,14 +125,14 @@ function App() {
|
||||
} else {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config for deprecated options and throws a warning in the logs
|
||||
* if it encounters one option from the deprecated.js list
|
||||
*
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
function checkDeprecatedOptions(userConfig) {
|
||||
@@ -99,11 +147,9 @@ function App() {
|
||||
|
||||
/**
|
||||
* Loads a specific module.
|
||||
*
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
function loadModule(module, callback) {
|
||||
function loadModule(module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
@@ -112,6 +158,14 @@ function App() {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${module}.js`;
|
||||
|
||||
try {
|
||||
fs.accessSync(moduleFile, fs.R_OK);
|
||||
} catch (e) {
|
||||
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
let loadHelper = true;
|
||||
@@ -127,7 +181,7 @@ function App() {
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
Log.log(`Check MagicMirror² version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
@@ -140,47 +194,29 @@ function App() {
|
||||
m.setPath(path.resolve(moduleFolder));
|
||||
nodeHelpers.push(m);
|
||||
|
||||
m.loaded(callback);
|
||||
} else {
|
||||
callback();
|
||||
m.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all modules.
|
||||
*
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
* @returns {Promise} A promise that is resolved when all modules been loaded
|
||||
*/
|
||||
function loadModules(modules, callback) {
|
||||
async function loadModules(modules) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function loadNextModule() {
|
||||
if (modules.length > 0) {
|
||||
const nextModule = modules[0];
|
||||
loadModule(nextModule, function () {
|
||||
modules = modules.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
} else {
|
||||
// All modules are loaded
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
for (let module of modules) {
|
||||
await loadModule(module);
|
||||
}
|
||||
|
||||
loadNextModule();
|
||||
Log.log("All module helpers loaded.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
*
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
@@ -203,43 +239,53 @@ function App() {
|
||||
/**
|
||||
* Start the core app.
|
||||
*
|
||||
* It loads the config, then it loads all modules. When it's done it
|
||||
* executes the callback with the config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after start
|
||||
* It loads the config, then it loads all modules.
|
||||
* @async
|
||||
* @returns {Promise<object>} the config used
|
||||
*/
|
||||
this.start = function (callback) {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
this.start = async function () {
|
||||
config = await loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
let modules = [];
|
||||
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
const server = new Server(config, function (app, io) {
|
||||
await loadModules(modules);
|
||||
|
||||
httpServer = new Server(config);
|
||||
const { app, io } = await httpServer.open();
|
||||
Log.log("Server started ...");
|
||||
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
|
||||
try {
|
||||
nodePromises.push(nodeHelper.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(nodePromises);
|
||||
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -247,13 +293,40 @@ function App() {
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
* @returns {Promise} A promise that is resolved when all node_helpers and
|
||||
* the http server has been closed
|
||||
*/
|
||||
this.stop = function () {
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
this.stop = async function () {
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
try {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
nodePromises.push(nodeHelper.stop());
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(nodePromises);
|
||||
|
||||
// Log errors that happened during async node_helper stopping
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
Log.log("Node_helpers stopped ...");
|
||||
|
||||
// To be able to stop the app even if it hasn't been started (when
|
||||
// running with Electron against another server)
|
||||
if (!httpServer) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return httpServer.close();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -263,12 +336,12 @@ function App() {
|
||||
* 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", () => {
|
||||
process.on("SIGINT", async () => {
|
||||
Log.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -276,12 +349,12 @@ function App() {
|
||||
* Listen to SIGTERM signals so we can stop everything when we
|
||||
* are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
process.on("SIGTERM", async () => {
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
*
|
||||
* Check the configuration file for errors
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Linter = require("eslint").Linter;
|
||||
const linter = new Linter();
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
@@ -18,7 +18,6 @@ const Utils = require(`${rootPath}/js/utils.js`);
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
@@ -52,7 +51,13 @@ function checkConfigFile() {
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
const errors = linter.verify(configFile);
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
|
||||
22
js/class.js
22
js/class.js
@@ -8,8 +8,8 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
(function () {
|
||||
var initializing = false;
|
||||
var fnTest = /xyz/.test(function () {
|
||||
let initializing = false;
|
||||
const fnTest = /xyz/.test(function () {
|
||||
xyz;
|
||||
})
|
||||
? /\b_super\b/
|
||||
@@ -20,27 +20,27 @@
|
||||
|
||||
// Create a new Class that inherits from this class
|
||||
Class.extend = function (prop) {
|
||||
var _super = this.prototype;
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
initializing = true;
|
||||
var prototype = new this();
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
|
||||
// Make a copy of all prototype properties, to prevent reference issues.
|
||||
for (var p in prototype) {
|
||||
for (const p in prototype) {
|
||||
prototype[p] = cloneObject(prototype[p]);
|
||||
}
|
||||
|
||||
// Copy the properties over onto the new prototype
|
||||
for (var name in prop) {
|
||||
for (const name in prop) {
|
||||
// Check if we're overwriting an existing function
|
||||
prototype[name] =
|
||||
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
? (function (name, fn) {
|
||||
return function () {
|
||||
var tmp = this._super;
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
var ret = fn.apply(this, arguments);
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
return ret;
|
||||
@@ -82,9 +82,7 @@
|
||||
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
*
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
@@ -92,8 +90,8 @@ function cloneObject(obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
var temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (var key in obj) {
|
||||
const temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (const key in obj) {
|
||||
temp[key] = cloneObject(obj[key]);
|
||||
|
||||
if (key === "lockStrings") {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* global mmPort */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var address = "localhost";
|
||||
var port = 8080;
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
port = mmPort;
|
||||
}
|
||||
var defaults = {
|
||||
const defaults = {
|
||||
address: address,
|
||||
port: port,
|
||||
basePath: "/",
|
||||
@@ -25,6 +25,14 @@ var defaults = {
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847
|
||||
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
|
||||
|
||||
// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled
|
||||
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
|
||||
checkServerInterval: 30 * 1000,
|
||||
reloadAfterServerRestart: false,
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -36,7 +44,7 @@ var defaults = {
|
||||
position: "upper_third",
|
||||
classes: "large thin",
|
||||
config: {
|
||||
text: "Magic Mirror<sup>2</sup>"
|
||||
text: "MagicMirror²"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -59,7 +67,7 @@ var defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>npm run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror Deprecated Config Options List
|
||||
/* MagicMirror² Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require("./app.js");
|
||||
const Log = require("logger");
|
||||
const core = require("./app");
|
||||
const Log = require("./logger");
|
||||
|
||||
// Config
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// If ELECTRON_DISABLE_GPU is set electron is started with --disable-gpu flag.
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
if (process.env.ELECTRON_DISABLE_GPU !== undefined) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
@@ -19,10 +25,20 @@ let mainWindow;
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
// see https://www.electronjs.org/docs/latest/api/screen
|
||||
// Create a window that fills the screen's available work area.
|
||||
let electronSize = (800, 600);
|
||||
try {
|
||||
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
|
||||
} catch {
|
||||
Log.warn("Could not get display size, using defaults ...");
|
||||
}
|
||||
|
||||
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
|
||||
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
width: electronSize.width,
|
||||
height: electronSize.height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
@@ -39,8 +55,11 @@ function createWindow() {
|
||||
if (config.kioskmode) {
|
||||
electronOptionsDefaults.kiosk = true;
|
||||
} else {
|
||||
electronOptionsDefaults.show = false;
|
||||
electronOptionsDefaults.frame = false;
|
||||
electronOptionsDefaults.transparent = true;
|
||||
electronOptionsDefaults.hasShadow = false;
|
||||
electronOptionsDefaults.fullscreen = true;
|
||||
electronOptionsDefaults.autoHideMenuBar = true;
|
||||
}
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
@@ -52,7 +71,7 @@ function createWindow() {
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
|
||||
let prefix;
|
||||
if (config["tls"] !== null && config["tls"]) {
|
||||
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
@@ -63,9 +82,19 @@ function createWindow() {
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
const devtools = new BrowserWindow(electronOptions);
|
||||
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
}
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// simulate mouse move to hide black cursor on start
|
||||
mainWindow.webContents.on("dom-ready", (event) => {
|
||||
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: 0, y: 0 });
|
||||
});
|
||||
|
||||
// Set responders for window events.
|
||||
mainWindow.on("closed", function () {
|
||||
mainWindow = null;
|
||||
@@ -86,18 +115,34 @@ function createWindow() {
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
//remove response headers that prevent sites of being embedded into iframes if configured
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
let curHeaders = details.responseHeaders;
|
||||
if (config["ignoreXOriginHeader"] || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/x-frame-options/i.test(header[0])));
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.on("ready", function () {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
if (config["ignoreContentSecurityPolicy"] || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/content-security-policy/i.test(header[0])));
|
||||
}
|
||||
|
||||
callback({ responseHeaders: curHeaders });
|
||||
});
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
app.quit();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
@@ -114,20 +159,39 @@ app.on("activate", function () {
|
||||
* 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) => {
|
||||
app.on("before-quit", async (event) => {
|
||||
Log.log("Shutting down server...");
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force-quit after 3 seconds.
|
||||
core.stop();
|
||||
await core.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle errors from self-signed certificates
|
||||
*/
|
||||
app.on("certificate-error", (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
|
||||
if (process.env.clientonly) {
|
||||
app.whenReady().then(() => {
|
||||
Log.log("Launching client viewer application.");
|
||||
createWindow();
|
||||
});
|
||||
}
|
||||
|
||||
// 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].includes(config.address)) {
|
||||
core.start(function (c) {
|
||||
core.start().then((c) => {
|
||||
config = c;
|
||||
app.whenReady().then(() => {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
239
js/loader.js
239
js/loader.js
@@ -1,64 +1,50 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Loader = (function () {
|
||||
const Loader = (function () {
|
||||
/* Create helper variables */
|
||||
|
||||
var loadedModuleFiles = [];
|
||||
var loadedFiles = [];
|
||||
var moduleObjects = [];
|
||||
const loadedModuleFiles = [];
|
||||
const loadedFiles = [];
|
||||
const moduleObjects = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
var loadModules = function () {
|
||||
var moduleData = getModuleData();
|
||||
|
||||
var loadNextModule = function () {
|
||||
if (moduleData.length > 0) {
|
||||
var nextModule = moduleData[0];
|
||||
loadModule(nextModule, function () {
|
||||
moduleData = moduleData.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
} else {
|
||||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
|
||||
loadFile(config.customCss, function () {
|
||||
// custom.css loaded. Start all modules.
|
||||
startModules();
|
||||
});
|
||||
const startModules = async function () {
|
||||
const modulePromises = [];
|
||||
for (const module of moduleObjects) {
|
||||
try {
|
||||
modulePromises.push(module.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${module.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
var startModules = function () {
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
module.start();
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(modulePromises);
|
||||
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
// Notify core of loaded modules.
|
||||
MM.modulesStarted(moduleObjects);
|
||||
|
||||
// Starting modules also hides any modules that have requested to be initially hidden
|
||||
for (let thisModule of moduleObjects) {
|
||||
for (const thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
Log.info(`Initially hiding ${thisModule.name}`);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
@@ -66,160 +52,145 @@ var Loader = (function () {
|
||||
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
var getAllModules = function () {
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
var getModuleData = function () {
|
||||
var modules = getAllModules();
|
||||
var moduleFiles = [];
|
||||
const getModuleData = function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
|
||||
for (var m in modules) {
|
||||
var moduleData = modules[m];
|
||||
var module = moduleData.module;
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = config.paths.modules + "/" + module;
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${config.paths.modules}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = config.paths.modules + "/default/" + module;
|
||||
moduleFolder = `${config.paths.modules}/default/${module}`;
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
moduleFiles.push({
|
||||
index: m,
|
||||
identifier: "module_" + m + "_" + module,
|
||||
index: index,
|
||||
identifier: `module_${index}_${module}`,
|
||||
name: moduleName,
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
path: `${moduleFolder}/`,
|
||||
file: `${moduleName}.js`,
|
||||
position: moduleData.position,
|
||||
animateIn: moduleData.animateIn,
|
||||
animateOut: moduleData.animateOut,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return moduleFiles;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load modules via ajax request and create module objects.s
|
||||
*
|
||||
* Load modules via ajax request and create module objects.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>} resolved when module is loaded
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var url = module.path + module.file;
|
||||
const loadModule = async function (module) {
|
||||
const url = module.path + module.file;
|
||||
|
||||
var afterLoad = function () {
|
||||
var moduleObject = Module.create(module.name);
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const afterLoad = async function () {
|
||||
const moduleObject = Module.create(module.name);
|
||||
if (moduleObject) {
|
||||
bootstrapModule(module, moduleObject, function () {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
await bootstrapModule(module, moduleObject);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadedModuleFiles.indexOf(url) !== -1) {
|
||||
afterLoad();
|
||||
await afterLoad();
|
||||
} else {
|
||||
loadFile(url, function () {
|
||||
await loadFile(url);
|
||||
loadedModuleFiles.push(url);
|
||||
afterLoad();
|
||||
});
|
||||
await afterLoad();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bootstrap modules by setting the module data and loading the scripts & styles.
|
||||
*
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
|
||||
const bootstrapModule = async function (module, mObj) {
|
||||
Log.info(`Bootstrapping module: ${module.name}`);
|
||||
mObj.setData(module);
|
||||
|
||||
mObj.loadScripts(function () {
|
||||
Log.log("Scripts loaded for: " + module.name);
|
||||
mObj.loadStyles(function () {
|
||||
Log.log("Styles loaded for: " + module.name);
|
||||
mObj.loadTranslations(function () {
|
||||
Log.log("Translations loaded for: " + module.name);
|
||||
await mObj.loadScripts();
|
||||
Log.log(`Scripts loaded for: ${module.name}`);
|
||||
|
||||
await mObj.loadStyles();
|
||||
Log.log(`Styles loaded for: ${module.name}`);
|
||||
|
||||
await mObj.loadTranslations();
|
||||
Log.log(`Translations loaded for: ${module.name}`);
|
||||
|
||||
moduleObjects.push(mObj);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a script or stylesheet by adding it to the dom.
|
||||
*
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise} resolved when the file is loaded
|
||||
*/
|
||||
var loadFile = function (fileName, callback) {
|
||||
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
const loadFile = async function (fileName) {
|
||||
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
let script, stylesheet;
|
||||
|
||||
switch (extension.toLowerCase()) {
|
||||
case "js":
|
||||
Log.log("Load script: " + fileName);
|
||||
var script = document.createElement("script");
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load script: ${fileName}`);
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
break;
|
||||
});
|
||||
case "css":
|
||||
Log.log("Load stylesheet: " + fileName);
|
||||
var stylesheet = document.createElement("link");
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load stylesheet: ${fileName}`);
|
||||
|
||||
stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.type = "text/css";
|
||||
stylesheet.href = fileName;
|
||||
stylesheet.onload = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,22 +199,40 @@ var Loader = (function () {
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
loadModules: async function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>} when all modules are loaded
|
||||
*/
|
||||
const loadNextModule = async function () {
|
||||
if (moduleData.length > 0) {
|
||||
const nextModule = moduleData[0];
|
||||
await loadModule(nextModule);
|
||||
moduleData = moduleData.slice(1);
|
||||
await loadNextModule();
|
||||
} else {
|
||||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
await loadFile(config.customCss);
|
||||
// custom.css loaded. Start all modules.
|
||||
await startModules();
|
||||
}
|
||||
};
|
||||
await loadNextModule();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a file (script or stylesheet).
|
||||
* Prevent double loading and search for files in the vendor folder.
|
||||
*
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Module} module The module that calls the loadFile function.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise} resolved when the file is loaded
|
||||
*/
|
||||
loadFile: function (fileName, module, callback) {
|
||||
loadFileForModule: async function (fileName, module) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
Log.log("File already loaded: " + fileName);
|
||||
callback();
|
||||
Log.log(`File already loaded: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,22 +240,20 @@ var Loader = (function () {
|
||||
// This is an absolute or relative path.
|
||||
// Load it and then return.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(fileName, callback);
|
||||
return;
|
||||
return loadFile(fileName);
|
||||
}
|
||||
|
||||
if (vendor[fileName] !== undefined) {
|
||||
// This file is available in the vendor folder.
|
||||
// Load it from this vendor folder.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(config.paths.vendor + "/" + vendor[fileName], callback);
|
||||
return;
|
||||
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
|
||||
}
|
||||
|
||||
// File not loaded yet.
|
||||
// Load it based on the module path.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
loadFile(module.file(fileName), callback);
|
||||
return loadFile(module.file(fileName));
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
35
js/logger.js
35
js/logger.js
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
@@ -9,12 +9,13 @@
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
});
|
||||
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
module.exports = factory(root.config);
|
||||
} else {
|
||||
@@ -22,7 +23,18 @@
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
const logLevel = {
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
// in nodejs and not running with jest
|
||||
enableLog = process.env.JEST_WORKER_ID === undefined;
|
||||
} else {
|
||||
// in browser and not running with jsdom
|
||||
enableLog = typeof window === "object" && window.name !== "jsdom";
|
||||
}
|
||||
|
||||
if (enableLog) {
|
||||
logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
@@ -45,6 +57,23 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
logLevel = {
|
||||
debug: function () {},
|
||||
log: function () {},
|
||||
info: function () {},
|
||||
warn: function () {},
|
||||
error: function () {},
|
||||
group: function () {},
|
||||
groupCollapsed: function () {},
|
||||
groupEnd: function () {},
|
||||
time: function () {},
|
||||
timeEnd: function () {},
|
||||
timeStamp: function () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function () {};
|
||||
}
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
||||
341
js/main.js
341
js/main.js
@@ -1,41 +1,45 @@
|
||||
/* global Loader, defaults, Translator */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var MM = (function () {
|
||||
var modules = [];
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Create dom objects for all modules that are configured for a specific position.
|
||||
*/
|
||||
var createDomObjects = function () {
|
||||
var domCreationPromises = [];
|
||||
const createDomObjects = function () {
|
||||
const domCreationPromises = [];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
if (typeof module.data.position !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapper = selectWrapper(module.data.position);
|
||||
let haveAnimateIn = null;
|
||||
// check if have valid animateIn in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn;
|
||||
|
||||
var dom = document.createElement("div");
|
||||
const wrapper = selectWrapper(module.data.position);
|
||||
|
||||
const 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.className = `module ${dom.className} ${module.data.classes}`;
|
||||
}
|
||||
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
var moduleHeader = document.createElement("header");
|
||||
const moduleHeader = document.createElement("header");
|
||||
moduleHeader.innerHTML = module.getHeader();
|
||||
moduleHeader.className = "module-header";
|
||||
dom.appendChild(moduleHeader);
|
||||
@@ -46,11 +50,16 @@ var MM = (function () {
|
||||
moduleHeader.style.display = "block;";
|
||||
}
|
||||
|
||||
var moduleContent = document.createElement("div");
|
||||
const moduleContent = document.createElement("div");
|
||||
moduleContent.className = "module-content";
|
||||
dom.appendChild(moduleContent);
|
||||
|
||||
var domCreationPromise = updateDom(module, 0);
|
||||
// create the domCreationPromise with AnimateCSS (with animateIn of module definition)
|
||||
// or just display it
|
||||
var domCreationPromise;
|
||||
if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true);
|
||||
else domCreationPromise = updateDom(module, 0);
|
||||
|
||||
domCreationPromises.push(domCreationPromise);
|
||||
domCreationPromise
|
||||
.then(function () {
|
||||
@@ -68,16 +77,14 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* @param {string} position The name of the position.
|
||||
*
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
* @returns {HTMLElement | void} the wrapper element
|
||||
*/
|
||||
var selectWrapper = function (position) {
|
||||
var classes = position.replace("_", " ");
|
||||
var parentWrapper = document.getElementsByClassName(classes);
|
||||
const selectWrapper = function (position) {
|
||||
const classes = position.replace("_", " ");
|
||||
const parentWrapper = document.getElementsByClassName(classes);
|
||||
if (parentWrapper.length > 0) {
|
||||
var wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
const wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
if (wrapper.length > 0) {
|
||||
return wrapper[0];
|
||||
}
|
||||
@@ -86,15 +93,14 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||
*/
|
||||
var sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (var m in modules) {
|
||||
var module = modules[m];
|
||||
const sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (const m in modules) {
|
||||
const module = modules[m];
|
||||
if (module !== sender && (!sendTo || module === sendTo)) {
|
||||
module.notificationReceived(notification, payload, sender);
|
||||
}
|
||||
@@ -103,16 +109,33 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
*
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
var updateDom = function (module, speed) {
|
||||
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
var newContentPromise = module.getDom();
|
||||
var newHeader = module.getHeader();
|
||||
let speed = updateOptions;
|
||||
let animateOut = null;
|
||||
let animateIn = null;
|
||||
if (typeof updateOptions === "object") {
|
||||
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
|
||||
speed = updateOptions.options.speed;
|
||||
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
|
||||
if (typeof updateOptions.options.animate === "object") {
|
||||
animateOut = updateOptions.options.animate.out;
|
||||
animateIn = updateOptions.options.animate.in;
|
||||
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
|
||||
}
|
||||
} else {
|
||||
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
|
||||
speed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const newHeader = module.getHeader();
|
||||
let newContentPromise = module.getDom();
|
||||
|
||||
if (!(newContentPromise instanceof Promise)) {
|
||||
// convert to a promise if not already one to avoid if/else's everywhere
|
||||
@@ -121,7 +144,7 @@ var MM = (function () {
|
||||
|
||||
newContentPromise
|
||||
.then(function (newContent) {
|
||||
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
|
||||
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
})
|
||||
@@ -131,15 +154,16 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom with the specified content
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* @param {string} [animateOut] AnimateCss animation name before hidden
|
||||
* @param {string} [animateIn] AnimateCss animation name on show
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
var updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
if (module.hidden || !speed) {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
@@ -158,42 +182,55 @@ var MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(module, speed / 2, function () {
|
||||
if (createAnimatedDom && animateIn !== null) {
|
||||
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2);
|
||||
showModule(module, speed, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(
|
||||
module,
|
||||
speed / 2,
|
||||
function () {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
{ animate: animateOut }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the content has changed.
|
||||
*
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* @returns {boolean} True if the module need an update, false otherwise
|
||||
*/
|
||||
var moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
|
||||
var headerNeedsUpdate = false;
|
||||
var contentNeedsUpdate = false;
|
||||
let headerNeedsUpdate = false;
|
||||
let contentNeedsUpdate;
|
||||
|
||||
if (headerWrapper.length > 0) {
|
||||
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
|
||||
}
|
||||
|
||||
var tempContentWrapper = document.createElement("div");
|
||||
const tempContentWrapper = document.createElement("div");
|
||||
tempContentWrapper.appendChild(newContent);
|
||||
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
|
||||
|
||||
@@ -202,18 +239,17 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the content of a module on screen.
|
||||
*
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*/
|
||||
var updateModuleContent = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const updateModuleContent = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return;
|
||||
}
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
|
||||
contentWrapper[0].innerHTML = "";
|
||||
contentWrapper[0].appendChild(newContent);
|
||||
@@ -228,15 +264,12 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
var hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
const hideModule = function (module, speed, callback, options = {}) {
|
||||
// set lockString if set in options.
|
||||
if (options.lockString) {
|
||||
// Log.log("Has lockstring: " + options.lockString);
|
||||
@@ -245,12 +278,53 @@ var MM = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateOut)
|
||||
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
|
||||
// can't be override with options.animate
|
||||
else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
||||
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
|
||||
module.hasAnimateOut = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
|
||||
// AnimateCSS is now done
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
moduleWrapper.style.position = "fixed";
|
||||
module.hasAnimateOut = false;
|
||||
|
||||
updateWrapperStates();
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
@@ -264,6 +338,7 @@ var MM = (function () {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") {
|
||||
@@ -274,18 +349,15 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
var showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
const showModule = function (module, speed, callback, options = {}) {
|
||||
// remove lockString if set in options.
|
||||
if (options.lockString) {
|
||||
var index = module.lockStrings.indexOf(options.lockString);
|
||||
const index = module.lockStrings.indexOf(options.lockString);
|
||||
if (index !== -1) {
|
||||
module.lockStrings.splice(index, 1);
|
||||
}
|
||||
@@ -294,39 +366,77 @@ var MM = (function () {
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
|
||||
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
||||
if (typeof options.onError === "function") {
|
||||
options.onError(new Error("LOCK_STRING_ACTIVE"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
|
||||
module.hidden = false;
|
||||
|
||||
// If forced show, clean current lockstrings.
|
||||
if (module.lockStrings.length !== 0 && options.force === true) {
|
||||
Log.log("Force show of module: " + module.name);
|
||||
Log.log(`Force show of module: ${module.name}`);
|
||||
module.lockStrings = [];
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
clearTimeout(module.showHideTimer);
|
||||
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
|
||||
// can't be override with options.animate
|
||||
else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
||||
|
||||
if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
// Restore the position. See hideModule() for more info.
|
||||
moduleWrapper.style.position = "static";
|
||||
moduleWrapper.classList.remove("hidden");
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
|
||||
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = false;
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback
|
||||
if (typeof callback === "function") {
|
||||
@@ -346,14 +456,14 @@ var MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
var updateWrapperStates = function () {
|
||||
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
const updateWrapperStates = function () {
|
||||
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
positions.forEach(function (position) {
|
||||
var wrapper = selectWrapper(position);
|
||||
var moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
var showWrapper = false;
|
||||
let showWrapper = false;
|
||||
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
|
||||
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
||||
showWrapper = true;
|
||||
@@ -367,7 +477,7 @@ var MM = (function () {
|
||||
/**
|
||||
* Loads the core config and combines it with the system defaults.
|
||||
*/
|
||||
var loadConfig = function () {
|
||||
const loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
/* eslint-disable */
|
||||
if (typeof config === "undefined") {
|
||||
@@ -382,51 +492,43 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
var setSelectionMethodsForModules = function (modules) {
|
||||
const setSelectionMethodsForModules = function (modules) {
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var withClass = function (className) {
|
||||
const withClass = function (className) {
|
||||
return modulesByClass(className, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptWithClass = function (className) {
|
||||
const exceptWithClass = function (className) {
|
||||
return modulesByClass(className, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters a collection of modules based on classname(s).
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var modulesByClass = function (className, include) {
|
||||
var searchClasses = className;
|
||||
const modulesByClass = function (className, include) {
|
||||
let searchClasses = className;
|
||||
if (typeof className === "string") {
|
||||
searchClasses = className.split(" ");
|
||||
}
|
||||
|
||||
var newModules = modules.filter(function (module) {
|
||||
var classes = module.data.classes.toLowerCase().split(" ");
|
||||
const newModules = modules.filter(function (module) {
|
||||
const classes = module.data.classes.toLowerCase().split(" ");
|
||||
|
||||
for (var c in searchClasses) {
|
||||
var searchClass = searchClasses[c];
|
||||
for (const searchClass of searchClasses) {
|
||||
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
|
||||
return include;
|
||||
}
|
||||
@@ -441,12 +543,11 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Removes a module instance from the collection.
|
||||
*
|
||||
* @param {object} module The module instance to remove from the collection.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptModule = function (module) {
|
||||
var newModules = modules.filter(function (mod) {
|
||||
const exceptModule = function (module) {
|
||||
const newModules = modules.filter(function (mod) {
|
||||
return mod.identifier !== module.identifier;
|
||||
});
|
||||
|
||||
@@ -456,10 +557,9 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
*
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
var enumerate = function (callback) {
|
||||
const enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
callback(module);
|
||||
});
|
||||
@@ -485,34 +585,53 @@ var MM = (function () {
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
init: async function () {
|
||||
Log.info("Initializing MagicMirror².");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
await Translator.loadCoreTranslations(config.language);
|
||||
await Loader.loadModules();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets called when all modules are started.
|
||||
*
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
modulesStarted: function (moduleObjects) {
|
||||
modules = [];
|
||||
let startUp = "";
|
||||
|
||||
moduleObjects.forEach((module) => modules.push(module));
|
||||
|
||||
Log.info("All modules started!");
|
||||
sendNotification("ALL_MODULES_STARTED");
|
||||
|
||||
createDomObjects();
|
||||
|
||||
if (config.reloadAfterServerRestart) {
|
||||
setInterval(async () => {
|
||||
// if server startup time has changed (which means server was restarted)
|
||||
// the client reloads the mm page
|
||||
try {
|
||||
const res = await fetch(`${location.protocol}//${location.host}/startup`);
|
||||
const curr = await res.text();
|
||||
if (startUp === "") startUp = curr;
|
||||
if (startUp !== curr) {
|
||||
startUp = "";
|
||||
window.location.reload(true);
|
||||
console.warn("Refreshing Website because server was restarted");
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(`MagicMirror not reachable: ${err}`);
|
||||
}
|
||||
}, config.checkServerInterval);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
@@ -539,11 +658,10 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
*/
|
||||
updateDom: function (module, speed) {
|
||||
updateDom: function (module, updateOptions) {
|
||||
if (!(module instanceof Module)) {
|
||||
Log.error("updateDom: Sender should be a module.");
|
||||
return;
|
||||
@@ -555,12 +673,11 @@ var MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
updateDom(module, updateOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
@@ -570,7 +687,6 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
@@ -583,7 +699,6 @@ var MM = (function () {
|
||||
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
@@ -604,11 +719,11 @@ if (typeof Object.assign !== "function") {
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError("Cannot convert undefined or null to object");
|
||||
}
|
||||
var output = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var source = arguments[index];
|
||||
const output = Object(target);
|
||||
for (let index = 1; index < arguments.length; index++) {
|
||||
const source = arguments[index];
|
||||
if (source !== undefined && source !== null) {
|
||||
for (var nextKey in source) {
|
||||
for (const nextKey in source) {
|
||||
if (source.hasOwnProperty(nextKey)) {
|
||||
output[nextKey] = source[nextKey];
|
||||
}
|
||||
|
||||
221
js/module.js
221
js/module.js
@@ -1,19 +1,18 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
*/
|
||||
var Module = Class.extend({
|
||||
const Module = Class.extend({
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
|
||||
// Set the minimum MagicMirror module version for this module.
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
|
||||
// Module config defaults.
|
||||
@@ -26,7 +25,7 @@ var Module = Class.extend({
|
||||
// visibility when hiding and showing module.
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjuck Environment,
|
||||
// Storage of the nunjucks Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
@@ -41,13 +40,12 @@ var Module = Class.extend({
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start: async function () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
*
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
@@ -56,7 +54,6 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
@@ -67,7 +64,6 @@ var Module = Class.extend({
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> -
|
||||
*
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
@@ -75,23 +71,21 @@ var Module = Class.extend({
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var self = this;
|
||||
return new Promise(function (resolve) {
|
||||
var div = document.createElement("div");
|
||||
var template = self.getTemplate();
|
||||
var templateData = self.getTemplateData();
|
||||
return new Promise((resolve) => {
|
||||
const div = document.createElement("div");
|
||||
const template = this.getTemplate();
|
||||
const templateData = this.getTemplateData();
|
||||
|
||||
// Check to see if we need to render a template string or a file.
|
||||
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||
// the template is a filename
|
||||
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
Log.error(err);
|
||||
}
|
||||
@@ -102,7 +96,7 @@ var Module = Class.extend({
|
||||
});
|
||||
} else {
|
||||
// the template is a template string.
|
||||
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
|
||||
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
|
||||
|
||||
resolve(div);
|
||||
}
|
||||
@@ -111,9 +105,8 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
|
||||
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader: function () {
|
||||
@@ -125,17 +118,15 @@ var Module = Class.extend({
|
||||
* This method needs to be subclassed if the module wants to use a template.
|
||||
* 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.
|
||||
*
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
|
||||
return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
@@ -143,8 +134,7 @@ var Module = Class.extend({
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by the Magic Mirror core when a notification arrives.
|
||||
*
|
||||
* Called by the MagicMirror² core when a notification arrives.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
@@ -160,7 +150,6 @@ var Module = Class.extend({
|
||||
/**
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
*
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function () {
|
||||
@@ -168,15 +157,13 @@ var Module = Class.extend({
|
||||
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, variables) {
|
||||
return nunjucks.runtime.markSafe(self.translate(str, variables));
|
||||
this._nunjucksEnvironment.addFilter("translate", (str, variables) => {
|
||||
return nunjucks.runtime.markSafe(this.translate(str, variables));
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
@@ -184,49 +171,48 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Called when a socket notification arrives.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/*
|
||||
/**
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
Log.log(`${this.name} is suspended.`);
|
||||
},
|
||||
|
||||
/*
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
* The methods below don"t need subclassing. *
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* @param {Module} data The module data
|
||||
* @param {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
this.name = data.name;
|
||||
this.identifier = data.identifier;
|
||||
this.hidden = false;
|
||||
this.hasAnimateIn = false;
|
||||
this.hasAnimateOut = false;
|
||||
|
||||
this.setConfig(data.config, data.configDeepMerge);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
@@ -237,7 +223,6 @@ var Module = Class.extend({
|
||||
/**
|
||||
* Returns a socket object. If it doesn't exist, it's created.
|
||||
* It also registers the notification callback.
|
||||
*
|
||||
* @returns {MMSocket} a socket object
|
||||
*/
|
||||
socket: function () {
|
||||
@@ -245,9 +230,8 @@ var Module = Class.extend({
|
||||
this._socket = new MMSocket(this.name);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._socket.setNotificationCallback(function (notification, payload) {
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this._socket.setNotificationCallback((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
|
||||
return this._socket;
|
||||
@@ -255,63 +239,56 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Retrieve the path to a module file.
|
||||
*
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
return (this.data.path + "/" + file).replace("//", "/");
|
||||
return `${this.data.path}/${file}`.replace("//", "/");
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadStyles: function (callback) {
|
||||
this.loadDependencies("getStyles", callback);
|
||||
loadStyles: function () {
|
||||
return this.loadDependencies("getStyles");
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadScripts: function (callback) {
|
||||
this.loadDependencies("getScripts", callback);
|
||||
loadScripts: function () {
|
||||
return this.loadDependencies("getScripts");
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to load all dependencies.
|
||||
*
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
var self = this;
|
||||
var dependencies = this[funcName]();
|
||||
loadDependencies: async function (funcName) {
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
var loadNextDependency = function () {
|
||||
const loadNextDependency = async () => {
|
||||
if (dependencies.length > 0) {
|
||||
var nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, self, function () {
|
||||
const nextDependency = dependencies[0];
|
||||
await Loader.loadFileForModule(nextDependency, this);
|
||||
dependencies = dependencies.slice(1);
|
||||
loadNextDependency();
|
||||
});
|
||||
await loadNextDependency();
|
||||
} else {
|
||||
callback();
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
loadNextDependency();
|
||||
await loadNextDependency();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all translations.
|
||||
*
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadTranslations(callback) {
|
||||
loadTranslations: async function () {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
@@ -319,7 +296,6 @@ var Module = Class.extend({
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,22 +303,18 @@ var Module = Class.extend({
|
||||
const translationsFallbackFile = translations[fallbackLanguage];
|
||||
|
||||
if (!translationFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
return;
|
||||
return Translator.load(this, translationsFallbackFile, true);
|
||||
}
|
||||
|
||||
Translator.load(this, translationFile, false, () => {
|
||||
await Translator.load(this, translationFile, false);
|
||||
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
return Translator.load(this, translationsFallbackFile, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* @param {string} key The key of the string to translate
|
||||
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
|
||||
* @param {string} [defaultValue] The default value with variables.
|
||||
@@ -357,16 +329,14 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Request an (animated) update of the module.
|
||||
*
|
||||
* @param {number} [speed] The speed of the animation.
|
||||
* @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates)
|
||||
*/
|
||||
updateDom: function (speed) {
|
||||
MM.updateDom(this, speed);
|
||||
updateDom: function (updateOptions) {
|
||||
MM.updateDom(this, updateOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
@@ -376,7 +346,6 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
@@ -386,62 +355,61 @@ var Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Hide this module.
|
||||
*
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide: function (speed, callback, options) {
|
||||
hide: function (speed, callback, options = {}) {
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = function () {};
|
||||
Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!");
|
||||
usedOptions = callback;
|
||||
usedCallback = function () {};
|
||||
}
|
||||
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.hideModule(
|
||||
self,
|
||||
this,
|
||||
speed,
|
||||
function () {
|
||||
self.suspend();
|
||||
callback();
|
||||
() => {
|
||||
this.suspend();
|
||||
usedCallback();
|
||||
},
|
||||
options
|
||||
usedOptions
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show this module.
|
||||
*
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = function () {};
|
||||
}
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
if (typeof callback === "object") {
|
||||
Log.error("Parameter mismatch in module.show: callback is not an optional parameter!");
|
||||
usedOptions = callback;
|
||||
usedCallback = function () {};
|
||||
}
|
||||
|
||||
MM.showModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.resume();
|
||||
callback();
|
||||
usedCallback();
|
||||
},
|
||||
options
|
||||
usedOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||
* Merging MagicMirror² (or other) default/config script by @bugsounet
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
@@ -459,14 +427,13 @@ var Module = Class.extend({
|
||||
* -------
|
||||
*
|
||||
* Todo: idea of Mich determinate what do you want to merge or not
|
||||
*
|
||||
* @param {object} result the initial object
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
function configMerge(result) {
|
||||
var stack = Array.prototype.slice.call(arguments, 1);
|
||||
var item;
|
||||
var key;
|
||||
const stack = Array.prototype.slice.call(arguments, 1);
|
||||
let item, key;
|
||||
|
||||
while (stack.length) {
|
||||
item = stack.shift();
|
||||
for (key in item) {
|
||||
@@ -494,46 +461,46 @@ Module.create = function (name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var moduleDefinition = Module.definitions[name];
|
||||
var clonedDefinition = cloneObject(moduleDefinition);
|
||||
const moduleDefinition = Module.definitions[name];
|
||||
const clonedDefinition = cloneObject(moduleDefinition);
|
||||
|
||||
// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
|
||||
var ModuleClass = Module.extend(clonedDefinition);
|
||||
const ModuleClass = Module.extend(clonedDefinition);
|
||||
|
||||
return new ModuleClass();
|
||||
};
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
|
||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log(`Check MagicMirror² version for module '${name}' - Minimum version: ${moduleDefinition.requiresVersion} - Current version: ${window.mmVersion}`);
|
||||
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
Log.warn(`Version is incorrect. Skip module: '${name}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Log.log(`Module registered: ${name}`);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
window.Module = Module;
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
var regExStrip0 = /(\.0+)+$/;
|
||||
var segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
var segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
var l = Math.min(segmentsA.length, segmentsB.length);
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
const l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
for (let i = 0; i < l; i++) {
|
||||
let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,55 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("logger");
|
||||
const express = require("express");
|
||||
const Log = require("logger");
|
||||
const Class = require("./class");
|
||||
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded(callback) {
|
||||
loaded() {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start() {
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
* Called when the MagicMirror server receives a `SIGINT`
|
||||
/**
|
||||
* Called when the MagicMirror² server receives a `SIGINT`
|
||||
* Close any open connections, stop any sub-processes and
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop() {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
/**
|
||||
* This method is called when a socket notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived(notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
/**
|
||||
* Set the module name.
|
||||
*
|
||||
* argument name string - Module name.
|
||||
* @param {string} name Module name.
|
||||
*/
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
/* setPath(path)
|
||||
/**
|
||||
* Set the module path.
|
||||
*
|
||||
* argument path string - Module path.
|
||||
* @param {string} path Module path.
|
||||
*/
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
@@ -113,6 +108,31 @@ const NodeHelper = Class.extend({
|
||||
}
|
||||
});
|
||||
|
||||
NodeHelper.checkFetchStatus = function (response) {
|
||||
// response.status >= 200 && response.status < 300
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Look at the specified error and return an appropriate error type, that
|
||||
* can be translated to a detailed error message
|
||||
* @param {Error} error the error from fetching something
|
||||
* @returns {string} the string of the detailed error message in the translations
|
||||
*/
|
||||
NodeHelper.checkFetchError = function (error) {
|
||||
let error_type = "MODULE_ERROR_UNSPECIFIED";
|
||||
if (error.code === "EAI_AGAIN") {
|
||||
error_type = "MODULE_ERROR_NO_CONNECTION";
|
||||
} else if (error.message === "Unauthorized") {
|
||||
error_type = "MODULE_ERROR_UNAUTHORIZED";
|
||||
}
|
||||
return error_type;
|
||||
};
|
||||
|
||||
NodeHelper.create = function (moduleDefinition) {
|
||||
return NodeHelper.extend(moduleDefinition);
|
||||
};
|
||||
|
||||
94
js/server.js
94
js/server.js
@@ -1,40 +1,49 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const helmet = require("helmet");
|
||||
const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @param {Function} callback Function called when done.
|
||||
* @class
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
function Server(config) {
|
||||
const app = express();
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
|
||||
const serverSockets = new Set();
|
||||
let server = null;
|
||||
|
||||
/**
|
||||
* Opens the server for incoming connections
|
||||
* @returns {Promise} A promise that is resolved when the server listens to connections
|
||||
*/
|
||||
this.open = function () {
|
||||
return new Promise((resolve) => {
|
||||
if (config.useHttps) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
server = require("https").Server(options, app);
|
||||
server = https.Server(options, app);
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
server = http.Server(app);
|
||||
}
|
||||
const io = require("socket.io")(server, {
|
||||
const io = socketio(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
@@ -42,8 +51,14 @@ function Server(config, callback) {
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
server.on("connection", (socket) => {
|
||||
serverSockets.add(socket);
|
||||
socket.on("close", () => {
|
||||
serverSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
server.listen(port, config.address || "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
@@ -53,45 +68,54 @@ function Server(config, callback) {
|
||||
app.use(function (req, res, next) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
return next();
|
||||
}
|
||||
Log.log(err.message);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use(helmet(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
// TODO add tests directory only when running tests?
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
app.get("/version", function (req, res) {
|
||||
res.send(global.version);
|
||||
app.get("/cors", async (req, res) => await cors(req, res));
|
||||
|
||||
app.get("/version", (req, res) => getVersion(req, res));
|
||||
|
||||
app.get("/config", (req, res) => getConfig(req, res));
|
||||
|
||||
app.get("/startup", (req, res) => getStartup(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
resolve({
|
||||
app,
|
||||
io
|
||||
});
|
||||
|
||||
app.get("/config", function (req, res) {
|
||||
res.send(config);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
app.get("/", function (req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
/**
|
||||
* Closes the server and destroys all lingering connections to it.
|
||||
* @returns {Promise} A promise that resolves when server has successfully shut down
|
||||
*/
|
||||
this.close = function () {
|
||||
return new Promise((resolve) => {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
res.send(html);
|
||||
server.close(resolve);
|
||||
});
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
||||
130
js/server_functions.js
Normal file
130
js/server_functions.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
const startUp = new Date();
|
||||
|
||||
/**
|
||||
* Gets the config.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getConfig(req, res) {
|
||||
res.send(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the startup time.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getStartup(req, res) {
|
||||
res.send(startUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
|
||||
*
|
||||
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
|
||||
*
|
||||
* Only the url-param of the input request url is required. It must be the last parameter.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
async function cors(req, res) {
|
||||
try {
|
||||
const urlRegEx = "url=(.+?)$";
|
||||
let url;
|
||||
|
||||
const match = new RegExp(urlRegEx, "g").exec(req.url);
|
||||
if (!match) {
|
||||
url = `invalid url: ${req.url}`;
|
||||
Log.error(url);
|
||||
res.send(url);
|
||||
} else {
|
||||
url = match[1];
|
||||
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
}
|
||||
const data = await response.text();
|
||||
res.send(data);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
res.send(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets headers and values to attach to the web request.
|
||||
* @param {string} url - The url containing the headers and values to send.
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
function getHeadersToSend(url) {
|
||||
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
|
||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (headersToSendMatch) {
|
||||
const headers = headersToSendMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
const keyValue = header.split(":");
|
||||
if (keyValue.length !== 2) {
|
||||
throw new Error(`Invalid format for header ${header}`);
|
||||
}
|
||||
headersToSend[keyValue[0]] = decodeURIComponent(keyValue[1]);
|
||||
}
|
||||
}
|
||||
return headersToSend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the headers expected from the response.
|
||||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
function geExpectedRecievedHeaders(url) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedRecievedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedRecievedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTML to display the magic mirror.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getHtml(req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
res.send(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MagicMirror version.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getVersion(req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
@@ -1,54 +1,50 @@
|
||||
/* global io */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* TODO add description
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var MMSocket = function (moduleName) {
|
||||
var self = this;
|
||||
|
||||
const MMSocket = function (moduleName) {
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
}
|
||||
|
||||
self.moduleName = moduleName;
|
||||
this.moduleName = moduleName;
|
||||
|
||||
// Private Methods
|
||||
var base = "/";
|
||||
let base = "/";
|
||||
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
|
||||
base = config.basePath;
|
||||
}
|
||||
self.socket = io("/" + self.moduleName, {
|
||||
path: base + "socket.io"
|
||||
this.socket = io(`/${this.moduleName}`, {
|
||||
path: `${base}socket.io`
|
||||
});
|
||||
var notificationCallback = function () {};
|
||||
|
||||
var onevent = self.socket.onevent;
|
||||
self.socket.onevent = function (packet) {
|
||||
var args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
let notificationCallback = function () {};
|
||||
|
||||
const onevent = this.socket.onevent;
|
||||
this.socket.onevent = (packet) => {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this.socket, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
onevent.call(this.socket, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
self.socket.on("*", function (notification, payload) {
|
||||
this.socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
notificationCallback(notification, payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Public Methods
|
||||
this.setNotificationCallback = function (callback) {
|
||||
this.setNotificationCallback = (callback) => {
|
||||
notificationCallback = callback;
|
||||
};
|
||||
|
||||
this.sendNotification = function (notification, payload) {
|
||||
if (typeof payload === "undefined") {
|
||||
payload = {};
|
||||
}
|
||||
self.socket.emit(notification, payload);
|
||||
this.sendNotification = (notification, payload = {}) => {
|
||||
this.socket.emit(notification, payload);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/* global translations */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Translator = (function () {
|
||||
const Translator = (function () {
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
* @returns {Promise<object>} the translations in the specified file
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
async function loadJSON(file) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise(function (resolve) {
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
@@ -25,12 +25,13 @@ var Translator = (function () {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(" loading json file =" + file + " failed");
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
callback(fileinfo);
|
||||
resolve(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -41,21 +42,17 @@ var Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a translation for a given key for a given module.
|
||||
*
|
||||
* @param {Module} module The module to load the translation for.
|
||||
* @param {string} key The key of the text to translate.
|
||||
* @param {object} variables The variables to use within the translation template (optional)
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (module, key, variables) {
|
||||
variables = variables || {}; //Empty object by default
|
||||
|
||||
translate: function (module, key, variables = {}) {
|
||||
/**
|
||||
* Combines template and variables like:
|
||||
* template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
* variables: {timeToWait: "2 hours", work: "painting"}
|
||||
* to: "Please wait for 2 hours before continuing with painting."
|
||||
*
|
||||
* @param {string} template Text with placeholder
|
||||
* @param {object} variables Variables for the placeholder
|
||||
* @returns {string} the template filled with the variables
|
||||
@@ -64,11 +61,12 @@ var Translator = (function () {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
}
|
||||
let templateToUse = template;
|
||||
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
|
||||
template = variables.fallback;
|
||||
templateToUse = variables.fallback;
|
||||
}
|
||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||
return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return varName in variables ? variables[varName] : `{${varName}}`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,62 +95,49 @@ var Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
* @param {Module} module The module to load the translation file for.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load(module, file, isFallback, callback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
|
||||
async load(module, file, isFallback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
loadJSON(module.file(file), (json) => {
|
||||
const json = await loadJSON(module.file(file));
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
loadCoreTranslations: async function (lang) {
|
||||
if (lang in translations) {
|
||||
Log.log("Loading core translation file: " + translations[lang]);
|
||||
loadJSON(translations[lang], (translations) => {
|
||||
this.coreTranslations = translations;
|
||||
});
|
||||
Log.log(`Loading core translation file: ${translations[lang]}`);
|
||||
this.coreTranslations = await loadJSON(translations[lang]);
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
this.loadCoreTranslationsFallback();
|
||||
await this.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the core translations fallback.
|
||||
* Load the core translations' fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function () {
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
break;
|
||||
}
|
||||
|
||||
loadCoreTranslationsFallback: async function () {
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], (translations) => {
|
||||
this.coreTranslationsFallback = translations;
|
||||
});
|
||||
Log.log(`Loading core translation fallback file: ${translations[first]}`);
|
||||
this.coreTranslationsFallback = await loadJSON(translations[first]);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.Translator = Translator;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
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,
|
||||
defaults?: object;
|
||||
[key: string]: any;
|
||||
start?(): void;
|
||||
getScripts?(): string[];
|
||||
getStyles?(): string[];
|
||||
getTranslations?(): object;
|
||||
getDom?(): HTMLElement;
|
||||
getHeader?(): string;
|
||||
getTemplate?(): string;
|
||||
getTemplateData?(): object;
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void;
|
||||
nunjucksEnvironment?(): void;
|
||||
socketNotificationReceived?(notification: string, payload: any): void;
|
||||
suspend?(): void;
|
||||
resume?(): void;
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
@@ -18,14 +21,14 @@ export declare const Module: {
|
||||
};
|
||||
|
||||
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,
|
||||
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;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
# Module: Alert
|
||||
|
||||
The alert module is one of the default modules of the MagicMirror. This module displays notifications from other modules.
|
||||
The alert module is one of the default modules of the MagicMirror². This module displays notifications from other modules.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/alert.html).
|
||||
|
||||
@@ -1,106 +1,104 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
defaults: {
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
effect: "slide",
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly",
|
||||
//time a notification is displayed in seconds
|
||||
display_time: 3500,
|
||||
//Position
|
||||
effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
display_time: 3500, // time a notification is displayed in seconds
|
||||
position: "center",
|
||||
//shown at startup
|
||||
welcome_message: false
|
||||
welcome_message: false // shown at startup
|
||||
},
|
||||
getScripts: function () {
|
||||
|
||||
getScripts() {
|
||||
return ["notificationFx.js"];
|
||||
},
|
||||
getStyles: function () {
|
||||
return ["notificationFx.css", "font-awesome.css"];
|
||||
|
||||
getStyles() {
|
||||
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
|
||||
getTranslations() {
|
||||
return {
|
||||
en: "translations/en.json",
|
||||
bg: "translations/bg.json",
|
||||
da: "translations/da.json",
|
||||
de: "translations/de.json",
|
||||
nl: "translations/nl.json"
|
||||
en: "translations/en.json",
|
||||
es: "translations/es.json",
|
||||
fr: "translations/fr.json",
|
||||
hu: "translations/hu.json",
|
||||
nl: "translations/nl.json",
|
||||
ru: "translations/ru.json",
|
||||
th: "translations/th.json"
|
||||
};
|
||||
},
|
||||
show_notification: function (message) {
|
||||
|
||||
getTemplate(type) {
|
||||
return `templates/${type}.njk`;
|
||||
},
|
||||
|
||||
async start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.effect === "slide") {
|
||||
this.config.effect = this.config.effect + "-" + this.config.position;
|
||||
this.config.effect = `${this.config.effect}-${this.config.position}`;
|
||||
}
|
||||
let msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
|
||||
if (this.config.welcome_message) {
|
||||
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
|
||||
await this.showNotification({ title: this.translate("sysTitle"), message });
|
||||
}
|
||||
if (message.message) {
|
||||
if (msg !== "") {
|
||||
msg += "<br />";
|
||||
},
|
||||
|
||||
notificationReceived(notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (payload.type === "notification") {
|
||||
this.showNotification(payload);
|
||||
} else {
|
||||
this.showAlert(payload, sender);
|
||||
}
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hideAlert(sender);
|
||||
}
|
||||
},
|
||||
|
||||
async showNotification(notification) {
|
||||
const message = await this.renderMessage(notification.templateName || "notification", notification);
|
||||
|
||||
new NotificationFx({
|
||||
message: msg,
|
||||
message,
|
||||
layout: "growl",
|
||||
effect: this.config.effect,
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
ttl: notification.timer || this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function (params, sender) {
|
||||
let image = "";
|
||||
//Set standard params if not provided by module
|
||||
if (typeof params.timer === "undefined") {
|
||||
params.timer = null;
|
||||
}
|
||||
if (typeof params.imageHeight === "undefined") {
|
||||
params.imageHeight = "80px";
|
||||
}
|
||||
if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") {
|
||||
params.imageUrl = null;
|
||||
} else if (typeof params.imageFA === "undefined") {
|
||||
image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />";
|
||||
} else if (typeof params.imageUrl === "undefined") {
|
||||
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />";
|
||||
}
|
||||
//Create overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "overlay";
|
||||
overlay.innerHTML += '<div class="black_overlay"></div>';
|
||||
document.body.insertBefore(overlay, document.body.firstChild);
|
||||
|
||||
async showAlert(alert, sender) {
|
||||
// If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender);
|
||||
this.hideAlert(sender, false);
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
let message = "";
|
||||
if (params.title) {
|
||||
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||
}
|
||||
if (params.message) {
|
||||
if (message !== "") {
|
||||
message += "<br />";
|
||||
// Add overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(true);
|
||||
}
|
||||
|
||||
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||
}
|
||||
const message = await this.renderMessage(alert.templateName || "alert", alert);
|
||||
|
||||
// Store alert in this.alerts
|
||||
this.alerts[sender.name] = new NotificationFx({
|
||||
message: image + message,
|
||||
message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
ttl: alert.timer,
|
||||
onClose: () => this.hideAlert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
@@ -108,60 +106,42 @@ Module.register("alert", {
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
// Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
if (alert.timer) {
|
||||
setTimeout(() => {
|
||||
this.hide_alert(sender);
|
||||
}, params.timer);
|
||||
this.hideAlert(sender);
|
||||
}, alert.timer);
|
||||
}
|
||||
},
|
||||
hide_alert: function (sender) {
|
||||
|
||||
hideAlert(sender, close = true) {
|
||||
// Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss();
|
||||
this.alerts[sender.name] = null;
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
delete this.alerts[sender.name];
|
||||
// Remove overlay
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
setPosition: function (pos) {
|
||||
//Add css to body depending on the set position for notifications
|
||||
const sheet = document.createElement("style");
|
||||
if (pos === "center") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";
|
||||
|
||||
renderMessage(type, data) {
|
||||
return new Promise((resolve) => {
|
||||
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
|
||||
if (err) {
|
||||
Log.error("Failed to render alert", err);
|
||||
}
|
||||
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);
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") {
|
||||
payload.type = "alert";
|
||||
|
||||
toggleBlur(add = false) {
|
||||
const method = add ? "add" : "remove";
|
||||
const modules = document.querySelectorAll(".module");
|
||||
for (const module of modules) {
|
||||
module.classList[method]("alert-blur");
|
||||
}
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type === "notification") {
|
||||
this.show_notification(payload);
|
||||
}
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hide_alert(sender);
|
||||
}
|
||||
},
|
||||
start: function () {
|
||||
this.alerts = {};
|
||||
this.setPosition(this.config.position);
|
||||
if (this.config.welcome_message) {
|
||||
if (this.config.welcome_message === true) {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
|
||||
} else {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
|
||||
}
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
*
|
||||
* Copyright 2014, Codrops
|
||||
* https://tympanus.net/codrops/
|
||||
* @param {object} window The window object
|
||||
*/
|
||||
(function (window) {
|
||||
/**
|
||||
* Extend one object with another one
|
||||
*
|
||||
* @param {object} a The object to extend
|
||||
* @param {object} b The object which extends the other, overwrites existing keys
|
||||
* @returns {object} The merged object
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
/**
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
@@ -78,7 +77,7 @@
|
||||
NotificationFx.prototype._init = function () {
|
||||
// create HTML structure
|
||||
this.ntf = document.createElement("div");
|
||||
this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type;
|
||||
this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
|
||||
let strinner = '<div class="ns-box-inner">';
|
||||
strinner += this.options.message;
|
||||
strinner += "</div>";
|
||||
@@ -122,8 +121,9 @@
|
||||
|
||||
/**
|
||||
* Dismiss the notification
|
||||
* @param {boolean} [close] call the onClose callback at the end
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function () {
|
||||
NotificationFx.prototype.dismiss = function (close = true) {
|
||||
this.active = false;
|
||||
clearTimeout(this.dismissttl);
|
||||
this.ntf.classList.remove("ns-show");
|
||||
@@ -131,7 +131,7 @@
|
||||
this.ntf.classList.add("ns-hide");
|
||||
|
||||
// callback
|
||||
this.options.onClose();
|
||||
if (close) this.options.onClose();
|
||||
}, 25);
|
||||
|
||||
// after animation ends remove ntf from the DOM
|
||||
|
||||
5
modules/default/alert/styles/center.css
Normal file
5
modules/default/alert/styles/center.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
4
modules/default/alert/styles/left.css
Normal file
4
modules/default/alert/styles/left.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
/* Based on work by https://tympanus.net/codrops/licensing/ */
|
||||
|
||||
.ns-box {
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
background-color: rgb(0 0 0 / 93%);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 1;
|
||||
color: black;
|
||||
font-size: 70%;
|
||||
position: relative;
|
||||
display: table;
|
||||
@@ -15,17 +14,17 @@
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
border-color: #666;
|
||||
border-color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.ns-alert {
|
||||
border-style: solid;
|
||||
border-color: #fff;
|
||||
border-color: var(--color-text-bright);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 3;
|
||||
color: white;
|
||||
color: var(--color-text-bright);
|
||||
font-size: 70%;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
@@ -40,12 +39,8 @@
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.black_overlay {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.alert-blur {
|
||||
filter: blur(2px) brightness(50%);
|
||||
}
|
||||
|
||||
[class^="ns-effect-"].ns-growl.ns-hide,
|
||||
@@ -60,15 +55,15 @@
|
||||
|
||||
.ns-effect-flip.ns-show,
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipFront;
|
||||
animation-name: anim-flip-front;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipBack;
|
||||
animation-name: anim-flip-back;
|
||||
}
|
||||
|
||||
@keyframes animFlipFront {
|
||||
@keyframes anim-flip-front {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, -90deg);
|
||||
}
|
||||
@@ -78,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFlipBack {
|
||||
@keyframes anim-flip-back {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
@@ -90,11 +85,11 @@
|
||||
|
||||
.ns-effect-bouncyflip.ns-show,
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInX;
|
||||
animation-name: flip-in-x;
|
||||
animation-duration: 0.8s;
|
||||
}
|
||||
|
||||
@keyframes flipInX {
|
||||
@keyframes flip-in-x {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
@@ -122,11 +117,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInXSimple;
|
||||
animation-name: flip-in-x-simple;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes flipInXSimple {
|
||||
@keyframes flip-in-x-simple {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
@@ -146,11 +141,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show {
|
||||
animation-name: animLoad;
|
||||
animation-name: anim-load;
|
||||
animation-duration: 1s;
|
||||
}
|
||||
|
||||
@keyframes animLoad {
|
||||
@keyframes anim-load {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale3d(0, 0.3, 1);
|
||||
@@ -163,7 +158,7 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@@ -175,15 +170,15 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-close {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-box-inner {
|
||||
animation-name: animFadeMove;
|
||||
animation-name: anim-fade-move;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@keyframes animFadeMove {
|
||||
@keyframes anim-fade-move {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 10px, 0);
|
||||
@@ -195,7 +190,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
@keyframes anim-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -207,11 +202,11 @@
|
||||
|
||||
.ns-effect-scale.ns-show,
|
||||
.ns-effect-scale.ns-hide {
|
||||
animation-name: animScale;
|
||||
animation-name: anim-scale;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animScale {
|
||||
@keyframes anim-scale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1);
|
||||
@@ -224,168 +219,169 @@
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-show {
|
||||
animation-name: animJelly;
|
||||
animation-name: anim-jelly;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
@keyframes anim-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animJelly {
|
||||
@keyframes anim-jelly {
|
||||
0% {
|
||||
transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
2.083333% {
|
||||
transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
4.166667% {
|
||||
transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
6.25% {
|
||||
transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
10.416667% {
|
||||
transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
12.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
14.583333% {
|
||||
transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
16.666667% {
|
||||
transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
18.75% {
|
||||
transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
20.833333% {
|
||||
transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
22.916667% {
|
||||
transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
27.083333% {
|
||||
transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
29.166667% {
|
||||
transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
31.25% {
|
||||
transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
35.416667% {
|
||||
transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
37.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
39.583333% {
|
||||
transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
43.75% {
|
||||
transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
45.833333% {
|
||||
transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
47.916667% {
|
||||
transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
52.083333% {
|
||||
transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
54.166667% {
|
||||
transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
56.25% {
|
||||
transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
60.416667% {
|
||||
transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
62.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
64.583333% {
|
||||
transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
68.75% {
|
||||
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
70.833333% {
|
||||
transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
72.916667% {
|
||||
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
@@ -393,47 +389,47 @@
|
||||
}
|
||||
|
||||
77.083333% {
|
||||
transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
79.166667% {
|
||||
transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
81.25% {
|
||||
transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
83.333333% {
|
||||
transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
85.416667% {
|
||||
transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
87.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
89.583333% {
|
||||
transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
91.666667% {
|
||||
transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
93.75% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
95.833333% {
|
||||
transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
97.916667% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
@@ -442,162 +438,162 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-show {
|
||||
animation-name: animSlideElasticLeft;
|
||||
animation-name: anim-slide-elastic-left;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticLeft {
|
||||
@keyframes anim-slide-elastic-left {
|
||||
0% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1);
|
||||
}
|
||||
|
||||
1.666667% {
|
||||
transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1);
|
||||
transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1);
|
||||
}
|
||||
|
||||
3.333333% {
|
||||
transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1);
|
||||
transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1);
|
||||
}
|
||||
|
||||
5% {
|
||||
transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1);
|
||||
transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1);
|
||||
}
|
||||
|
||||
6.666667% {
|
||||
transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1);
|
||||
transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1);
|
||||
transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1);
|
||||
transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1);
|
||||
}
|
||||
|
||||
11.666667% {
|
||||
transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1);
|
||||
transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1);
|
||||
}
|
||||
|
||||
13.333333% {
|
||||
transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1);
|
||||
transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1);
|
||||
transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1);
|
||||
}
|
||||
|
||||
16.666667% {
|
||||
transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1);
|
||||
transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1);
|
||||
}
|
||||
|
||||
18.333333% {
|
||||
transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1);
|
||||
transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1);
|
||||
transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1);
|
||||
}
|
||||
|
||||
21.666667% {
|
||||
transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1);
|
||||
transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1);
|
||||
}
|
||||
|
||||
23.333333% {
|
||||
transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1);
|
||||
transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1);
|
||||
}
|
||||
|
||||
26.666667% {
|
||||
transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1);
|
||||
}
|
||||
|
||||
28.333333% {
|
||||
transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1);
|
||||
transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1);
|
||||
}
|
||||
|
||||
31.666667% {
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1);
|
||||
transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1);
|
||||
}
|
||||
|
||||
36.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1);
|
||||
}
|
||||
|
||||
38.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1);
|
||||
}
|
||||
|
||||
43.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1);
|
||||
}
|
||||
|
||||
46.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1);
|
||||
}
|
||||
|
||||
48.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1);
|
||||
}
|
||||
|
||||
51.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1);
|
||||
}
|
||||
|
||||
53.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1);
|
||||
}
|
||||
|
||||
56.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1);
|
||||
}
|
||||
|
||||
61.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1);
|
||||
}
|
||||
|
||||
63.333333% {
|
||||
@@ -605,67 +601,67 @@
|
||||
}
|
||||
|
||||
65% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1);
|
||||
}
|
||||
|
||||
68.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1);
|
||||
}
|
||||
|
||||
71.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1);
|
||||
}
|
||||
|
||||
73.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1);
|
||||
}
|
||||
|
||||
76.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1);
|
||||
}
|
||||
|
||||
78.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1);
|
||||
}
|
||||
|
||||
81.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1);
|
||||
}
|
||||
|
||||
83.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1);
|
||||
}
|
||||
|
||||
86.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
88.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
91.666667% {
|
||||
@@ -677,15 +673,15 @@
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
96.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
98.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1);
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
@@ -694,11 +690,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-hide {
|
||||
animation-name: animSlideLeft;
|
||||
animation-name: anim-slide-left;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
@keyframes anim-slide-left {
|
||||
0% {
|
||||
transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0);
|
||||
}
|
||||
@@ -709,10 +705,10 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-show {
|
||||
animation: animSlideElasticRight 2000ms linear both;
|
||||
animation: anim-slide-elastic-right 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticRight {
|
||||
@keyframes anim-slide-elastic-right {
|
||||
0% {
|
||||
transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1);
|
||||
}
|
||||
@@ -791,11 +787,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-hide {
|
||||
animation-name: animSlideRight;
|
||||
animation-name: anim-slide-right;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideRight {
|
||||
@keyframes anim-slide-right {
|
||||
0% {
|
||||
transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0);
|
||||
}
|
||||
@@ -806,10 +802,10 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-show {
|
||||
animation: animSlideElasticCenter 2000ms linear both;
|
||||
animation: anim-slide-elastic-center 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticCenter {
|
||||
@keyframes anim-slide-elastic-center {
|
||||
0% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1);
|
||||
}
|
||||
@@ -888,11 +884,11 @@
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-hide {
|
||||
animation-name: animSlideCenter;
|
||||
animation-name: anim-slide-center;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideCenter {
|
||||
@keyframes anim-slide-center {
|
||||
0% {
|
||||
transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0);
|
||||
}
|
||||
@@ -904,11 +900,11 @@
|
||||
|
||||
.ns-effect-genie.ns-show,
|
||||
.ns-effect-genie.ns-hide {
|
||||
animation-name: animGenie;
|
||||
animation-name: anim-genie;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes animGenie {
|
||||
@keyframes anim-genie {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1);
|
||||
4
modules/default/alert/styles/right.css
Normal file
4
modules/default/alert/styles/right.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
20
modules/default/alert/templates/alert.njk
Normal file
20
modules/default/alert/templates/alert.njk
Normal file
@@ -0,0 +1,20 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}"
|
||||
height="{{ imageHeight }}"
|
||||
style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
7
modules/default/alert/templates/notification.njk
Normal file
7
modules/default/alert/templates/notification.njk
Normal file
@@ -0,0 +1,7 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror нотификация",
|
||||
"sysTitle": "MagicMirror² нотификация",
|
||||
"welcome": "Добре дошли, стартирането беше успешно"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notifikation",
|
||||
"sysTitle": "MagicMirror² Notifikation",
|
||||
"welcome": "Velkommen, modulet er succesfuldt startet!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Benachrichtigung",
|
||||
"sysTitle": "MagicMirror² Benachrichtigung",
|
||||
"welcome": "Willkommen, Start war erfolgreich!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"sysTitle": "MagicMirror² Notification",
|
||||
"welcome": "Welcome, start was successful!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificaciones",
|
||||
"sysTitle": "MagicMirror² Notificaciones",
|
||||
"welcome": "Bienvenido, ¡se iniciado correctamente!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"sysTitle": "MagicMirror² Notification",
|
||||
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror értesítés",
|
||||
"sysTitle": "MagicMirror² értesítés",
|
||||
"welcome": "Üdvözöljük, indulás sikeres!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificatie",
|
||||
"sysTitle": "MagicMirror² Notificatie",
|
||||
"welcome": "Welkom, Succesvol gestart!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Уведомление",
|
||||
"sysTitle": "MagicMirror² Уведомление",
|
||||
"welcome": "Добро пожаловать, старт был успешным!"
|
||||
}
|
||||
|
||||
4
modules/default/alert/translations/th.json
Normal file
4
modules/default/alert/translations/th.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "การแจ้งเตือน MagicMirror²",
|
||||
"welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!"
|
||||
}
|
||||
2
modules/default/calendar/README.md
Executable file → Normal file
2
modules/default/calendar/README.md
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
# Module: Calendar
|
||||
|
||||
The `calendar` module is one of the default modules of the MagicMirror.
|
||||
The `calendar` module is one of the default modules of the MagicMirror².
|
||||
This module displays events from a public .ical calendar. It can combine multiple calendars.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/calendar.html).
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
.calendar .symbol {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
font-size: 80%;
|
||||
vertical-align: top;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.calendar .symbol span {
|
||||
display: inline-block;
|
||||
transform: translate(0, 2px);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.calendar .title {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.calendar .time {
|
||||
|
||||
524
modules/default/calendar/calendar.js
Executable file → Normal file
524
modules/default/calendar/calendar.js
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
/* global cloneObject */
|
||||
/* global CalendarUtils, cloneObject */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -12,18 +12,20 @@ Module.register("calendar", {
|
||||
maximumEntries: 10, // Total Maximum Entries
|
||||
maximumNumberOfDays: 365,
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
pastDaysCount: 0,
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbolClassName: "fas fa-fw fa-",
|
||||
showLocation: false,
|
||||
displayRepeatingCountTitle: false,
|
||||
defaultRepeatingCountTitle: "",
|
||||
maxTitleLength: 25,
|
||||
maxLocationTitleLength: 25,
|
||||
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
||||
wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
|
||||
wrapLocationEvents: false,
|
||||
maxTitleLines: 3,
|
||||
maxEventTitleLines: 3,
|
||||
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
|
||||
fetchInterval: 60 * 60 * 1000, // Update every hour
|
||||
animationSpeed: 2000,
|
||||
fade: true,
|
||||
urgency: 7,
|
||||
@@ -37,13 +39,14 @@ Module.register("calendar", {
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: false,
|
||||
hideDuplicates: true,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar",
|
||||
symbol: "calendar-alt",
|
||||
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
||||
}
|
||||
],
|
||||
@@ -59,7 +62,13 @@ Module.register("calendar", {
|
||||
sliceMultiDayEvents: false,
|
||||
broadcastPastEvents: false,
|
||||
nextDaysRelative: false,
|
||||
selfSignedCert: false
|
||||
selfSignedCert: false,
|
||||
coloredText: false,
|
||||
coloredBorder: false,
|
||||
coloredSymbol: false,
|
||||
coloredBackground: false,
|
||||
limitDaysNeverSkip: false,
|
||||
flipDateHeaderTitle: false
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
@@ -71,7 +80,7 @@ Module.register("calendar", {
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
return ["calendarutils.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
@@ -84,10 +93,23 @@ Module.register("calendar", {
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting module: " + this.name);
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.colored) {
|
||||
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = true;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
if (this.config.coloredSymbolOnly) {
|
||||
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = false;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
|
||||
// Set locale.
|
||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||
moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
|
||||
|
||||
// clear data holder before start
|
||||
this.calendarData = {};
|
||||
@@ -101,6 +123,7 @@ Module.register("calendar", {
|
||||
const calendarConfig = {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
pastDaysCount: calendar.pastDaysCount,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
selfSignedCert: calendar.selfSignedCert
|
||||
};
|
||||
@@ -129,10 +152,25 @@ Module.register("calendar", {
|
||||
// fetcher till cycle
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
// Refresh the DOM every minute if needed: When using relative date format for events that start
|
||||
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
|
||||
setTimeout(
|
||||
() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(1);
|
||||
}, ONE_MINUTE);
|
||||
},
|
||||
ONE_MINUTE - (new Date() % ONE_MINUTE)
|
||||
);
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
}
|
||||
|
||||
if (this.identifier !== payload.id) {
|
||||
return;
|
||||
}
|
||||
@@ -140,17 +178,17 @@ Module.register("calendar", {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
if (this.hasCalendarURL(payload.url)) {
|
||||
this.calendarData[payload.url] = payload.events;
|
||||
this.error = null;
|
||||
this.loaded = true;
|
||||
|
||||
if (this.config.broadcastEvents) {
|
||||
this.broadcastEvents();
|
||||
}
|
||||
}
|
||||
} else if (notification === "FETCH_ERROR") {
|
||||
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
|
||||
} else if (notification === "CALENDAR_ERROR") {
|
||||
let error_message = this.translate(payload.error_type);
|
||||
this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
|
||||
this.loaded = true;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
Log.error("Calendar Error. Incorrect url: " + payload.url);
|
||||
}
|
||||
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
@@ -158,19 +196,24 @@ Module.register("calendar", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
// Define second, minute, hour, and day constants
|
||||
const oneSecond = 1000; // 1,000 milliseconds
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
const events = this.createEventList();
|
||||
const events = this.createEventList(true);
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (this.error) {
|
||||
wrapper.innerHTML = this.error;
|
||||
wrapper.className = `${this.config.tableClass} dimmed`;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
wrapper.className = `${this.config.tableClass} dimmed`;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -193,7 +236,12 @@ Module.register("calendar", {
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
const dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
dateRow.className = "dateheader normal";
|
||||
if (event.today) dateRow.className += " today";
|
||||
else if (event.dayBeforeYesterday) dateRow.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) dateRow.className += " yesterday";
|
||||
else if (event.tomorrow) dateRow.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) dateRow.className += " dayAfterTomorrow";
|
||||
|
||||
const dateCell = document.createElement("td");
|
||||
dateCell.colSpan = "3";
|
||||
@@ -214,38 +262,39 @@ Module.register("calendar", {
|
||||
|
||||
const eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
if (this.config.coloredText) {
|
||||
eventWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
eventWrapper.className = "normal event";
|
||||
if (this.config.coloredBackground) {
|
||||
eventWrapper.style.backgroundColor = this.colorForUrl(event.url, true);
|
||||
}
|
||||
|
||||
if (this.config.coloredBorder) {
|
||||
eventWrapper.style.borderColor = this.colorForUrl(event.url, false);
|
||||
}
|
||||
|
||||
eventWrapper.className = "event-wrapper normal event";
|
||||
if (event.today) eventWrapper.className += " today";
|
||||
else if (event.dayBeforeYesterday) eventWrapper.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) eventWrapper.className += " yesterday";
|
||||
else if (event.tomorrow) eventWrapper.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) eventWrapper.className += " dayAfterTomorrow";
|
||||
|
||||
const symbolWrapper = document.createElement("td");
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
if (this.config.coloredSymbol) {
|
||||
symbolWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
||||
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = this.config.customEvents[ev].symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
symbols.forEach((s, index) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + s;
|
||||
symbol.className = s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
@@ -268,55 +317,67 @@ Module.register("calendar", {
|
||||
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
yearDiff = thisYear - event.firstYear;
|
||||
|
||||
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
|
||||
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Color events if custom color is specified
|
||||
// Color events if custom color or eventClass are specified
|
||||
if (this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
// Respect parameter ColoredSymbolOnly also for custom events
|
||||
if (!this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
if (this.config.coloredText) {
|
||||
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
titleWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
}
|
||||
if (this.config.displaySymbol) {
|
||||
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
if (this.config.displaySymbol && this.config.coloredSymbol) {
|
||||
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
|
||||
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
|
||||
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
const titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
if (!this.config.coloredText) {
|
||||
titleWrapper.className = `title bright ${titleClass}`;
|
||||
} else {
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
titleWrapper.className = `title ${titleClass}`;
|
||||
}
|
||||
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
titleWrapper.classList.add("align-left");
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.align = "right";
|
||||
|
||||
// Add endDate to dataheaders if showEnd is enabled
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
|
||||
if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
|
||||
}
|
||||
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
@@ -325,97 +386,134 @@ Module.register("calendar", {
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
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;
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
event.endDate -= ONE_SECOND;
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
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) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
sameDay: "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd"
|
||||
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
||||
nextDay: `[${this.translate("TOMORROW")}]`,
|
||||
nextWeek: "dddd",
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
);
|
||||
}
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
|
||||
}
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
if (this.config.showLocation) {
|
||||
if (event.location !== false) {
|
||||
const locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
locationRow.className = "event-wrapper-location normal xsmall light";
|
||||
if (event.today) locationRow.className += " today";
|
||||
else if (event.dayBeforeYesterday) locationRow.className += " dayBeforeYesterday";
|
||||
else if (event.yesterday) locationRow.className += " yesterday";
|
||||
else if (event.tomorrow) locationRow.className += " tomorrow";
|
||||
else if (event.dayAfterTomorrow) locationRow.className += " dayAfterTomorrow";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
const symbolCell = document.createElement("td");
|
||||
locationRow.appendChild(symbolCell);
|
||||
}
|
||||
|
||||
if (this.config.coloredText) {
|
||||
locationRow.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
|
||||
}
|
||||
|
||||
if (this.config.coloredBackground) {
|
||||
locationRow.style.backgroundColor = this.colorForUrl(event.url, true);
|
||||
}
|
||||
|
||||
if (this.config.coloredBorder) {
|
||||
locationRow.style.borderColor = this.colorForUrl(event.url, false);
|
||||
}
|
||||
|
||||
const descCell = document.createElement("td");
|
||||
descCell.className = "location";
|
||||
descCell.colSpan = "2";
|
||||
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
|
||||
|
||||
const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
|
||||
descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
|
||||
locationRow.appendChild(descCell);
|
||||
|
||||
wrapper.appendChild(locationRow);
|
||||
@@ -431,31 +529,8 @@ Module.register("calendar", {
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
case 12: {
|
||||
return { longDateFormat: { LT: "h:mm A" } };
|
||||
}
|
||||
case 24: {
|
||||
return { longDateFormat: { LT: "HH:mm" } };
|
||||
}
|
||||
default: {
|
||||
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if this config contains the calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
@@ -471,10 +546,15 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
*
|
||||
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function () {
|
||||
createEventList: function (limitNumberOfEntries) {
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
@@ -482,42 +562,51 @@ Module.register("calendar", {
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
|
||||
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
if (this.config.hidePrivate) {
|
||||
if (event.class === "PRIVATE") {
|
||||
if (this.config.hidePrivate && event.class === "PRIVATE") {
|
||||
// do not add the current event, skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.config.hideOngoing) {
|
||||
if (event.startDate < now) {
|
||||
if (limitNumberOfEntries) {
|
||||
if (event.endDate < maxPastDaysCompare) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
if (this.config.hideOngoing && event.startDate < now) {
|
||||
continue;
|
||||
}
|
||||
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
}
|
||||
if (--remainingEntries < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
event.url = calendarUrl;
|
||||
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
|
||||
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
|
||||
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
const splitEvents = [];
|
||||
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||
thisEvent.title += ` (${count}/${maxCount})`;
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
@@ -525,7 +614,9 @@ Module.register("calendar", {
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
event.title += ` (${count}/${maxCount})`;
|
||||
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
splitEvents.push(event);
|
||||
|
||||
for (let splitEvent of splitEvents) {
|
||||
@@ -543,6 +634,10 @@ Module.register("calendar", {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
if (!limitNumberOfEntries) {
|
||||
return events;
|
||||
}
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
@@ -555,7 +650,7 @@ Module.register("calendar", {
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
}
|
||||
days++;
|
||||
@@ -575,7 +670,7 @@ Module.register("calendar", {
|
||||
|
||||
listContainsEvent: function (eventList, event) {
|
||||
for (const evt of eventList) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -584,7 +679,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Requests node helper to add calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url to add
|
||||
* @param {object} auth The authentication method and credentials
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
@@ -596,7 +690,8 @@ Module.register("calendar", {
|
||||
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
|
||||
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
||||
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
||||
fetchInterval: this.config.fetchInterval,
|
||||
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
|
||||
fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
|
||||
symbolClass: calendarConfig.symbolClass,
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
@@ -608,7 +703,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the symbols for a specific event.
|
||||
*
|
||||
* @param {object} event Event to look for.
|
||||
* @returns {string[]} The symbols
|
||||
*/
|
||||
@@ -623,6 +717,19 @@ Module.register("calendar", {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
// If custom symbol is set, replace event symbol
|
||||
for (let ev of this.config.customEvents) {
|
||||
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
|
||||
let needle = new RegExp(ev.keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
// Get the default prefix for this class name and add to the custom symbol provided
|
||||
const className = this.getCalendarProperty(event.url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||
symbols[0] = className + ev.symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
},
|
||||
|
||||
@@ -636,7 +743,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the symbolClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the symbols of the calendar
|
||||
*/
|
||||
@@ -646,7 +752,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the titleClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the title of the calendar
|
||||
*/
|
||||
@@ -656,7 +761,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the timeClass for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the time of the calendar
|
||||
*/
|
||||
@@ -666,7 +770,6 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the calendar name for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The name of the calendar
|
||||
*/
|
||||
@@ -676,17 +779,16 @@ Module.register("calendar", {
|
||||
|
||||
/**
|
||||
* Retrieves the color for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @param {boolean} isBg Determines if we fetch the bgColor or not
|
||||
* @returns {string} The color
|
||||
*/
|
||||
colorForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "color", "#fff");
|
||||
colorForUrl: function (url, isBg) {
|
||||
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the count title for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The title
|
||||
*/
|
||||
@@ -694,9 +796,26 @@ Module.register("calendar", {
|
||||
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the maximum entry count for a specific calendar url.
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum entry count
|
||||
*/
|
||||
maximumEntriesForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum past days count
|
||||
*/
|
||||
maximumPastDaysForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the property for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
@@ -714,6 +833,11 @@ Module.register("calendar", {
|
||||
|
||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
|
||||
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||
p = className + p;
|
||||
}
|
||||
|
||||
if (!(p instanceof Array)) p = [p];
|
||||
return p;
|
||||
},
|
||||
@@ -722,118 +846,18 @@ Module.register("calendar", {
|
||||
return !!this.getCalendarProperty(url, property, undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (wrapEvents === true) {
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const 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 {
|
||||
temp += word + "<br>";
|
||||
}
|
||||
currentLine = "";
|
||||
}
|
||||
}
|
||||
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return string.trim().slice(0, maxLength) + "…";
|
||||
} else {
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
*
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
*
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLength The max length of the string
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
title = title.replace(needle, replacement);
|
||||
}
|
||||
|
||||
title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
broadcastEvents: function () {
|
||||
const eventList = [];
|
||||
for (const url in this.calendarData) {
|
||||
for (const ev of this.calendarData[url]) {
|
||||
const event = cloneObject(ev);
|
||||
const eventList = this.createEventList(false);
|
||||
for (const event of eventList) {
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
event.calendarName = this.calendarNameForUrl(event.url);
|
||||
event.color = this.colorForUrl(event.url, false);
|
||||
delete event.url;
|
||||
eventList.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
eventList.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
this.sendNotification("CALENDAR_EVENTS", eventList);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
const Log = require("logger");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("node-fetch");
|
||||
const digest = require("digest-fetch");
|
||||
|
||||
const https = require("https");
|
||||
const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -37,10 +37,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
@@ -50,49 +49,38 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
}
|
||||
if (auth) {
|
||||
if (auth.method === "bearer") {
|
||||
headers.Authorization = "Bearer " + auth.pass;
|
||||
} else if (auth.method === "digest") {
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
headers.Authorization = `Bearer ${auth.pass}`;
|
||||
} else {
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
}
|
||||
|
||||
fetcher
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchFailedCallback(this, response.statusText);
|
||||
scheduleTimer();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
fetch(url, { headers: headers, agent: httpsAgent })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
Log.debug(`parsed data=${JSON.stringify(data)}`);
|
||||
events = CalendarFetcherUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
maximumNumberOfDays
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error.message);
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,13 +107,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the on success callback
|
||||
*
|
||||
* @param {Function} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
@@ -134,7 +121,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
@@ -143,7 +129,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
@@ -152,7 +137,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
*
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function () {
|
||||
|
||||
608
modules/default/calendar/calendarfetcherutils.js
Normal file
608
modules/default/calendar/calendarfetcherutils.js
Normal file
@@ -0,0 +1,608 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Fetcher Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const path = require("path");
|
||||
const moment = require("moment");
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
const CalendarFetcherUtils = {
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
* @param {object} event the event which needs adjustment
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug(`initial tz=${event.start.tz}`);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug(`corrected TZ=${tz}`);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug(`corrected tz=${event.start.tz}`);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug(`defined offset=${start_offset} hours`);
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug(`ical offset=${current_offset} date=${date}`);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug(`net mins=${current_offset * 60 - x}`);
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug(`adjusted date=${event.start}`);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug(`start date/time=${es.toDate()}`);
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug(`start offset=${start_offset}`);
|
||||
|
||||
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug(`event date=${mm.toDate()}`);
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug(`adjustHours=${adjustHours}`);
|
||||
return adjustHours;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the events from ical according to the given config
|
||||
* @param {object} data the calendar data from ical
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug(`Event:\n${JSON.stringify(event)}`);
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${startDate.toDate()}`);
|
||||
Log.debug(`end:: ${endDate.toDate()}`);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
Log.debug(`title: ${title}`);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.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 (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && 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);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
|
||||
Log.debug(`pastLocal: ${pastLocal}`);
|
||||
Log.debug(`futureLocal: ${futureLocal}`);
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug(`event.recurrences: ${event.recurrences}`);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// set the time information in the date to equal the time information in the event
|
||||
date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds());
|
||||
|
||||
// Get the offset of today where we are processing
|
||||
// This will be the correction, we need to apply.
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// For full day events, the time might be off from RRULE/Luxon problem
|
||||
// Get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
|
||||
// Reduce the time by the following offset.
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
|
||||
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
|
||||
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
// reduce the time by the offset
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 fulldate is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 fulldate is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Reduce the time by t:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date1 is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`new recurring date2 is ${date}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
|
||||
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${description}`);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// End recurring event parsing.
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
title = event.description;
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined title filter should apply
|
||||
* @param {string} title the title of the event
|
||||
* @param {string} filter the string to look for, can be a regex also
|
||||
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
|
||||
* @param {string} regexFlags flags that should be applied to the regex
|
||||
* @returns {boolean} True if the title should be filtered out, false otherwise
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
let regexFilter = filter;
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
regexFilter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
return new RegExp(regexFilter, regexFlags).test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CalendarFetcherUtils;
|
||||
}
|
||||
@@ -1,597 +1,114 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const moment = require("moment");
|
||||
const path = require("path");
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger.js");
|
||||
|
||||
const CalendarUtils = {
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
*
|
||||
* @param {object} event
|
||||
* @param {Date} date
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
* Capitalize the first letter of a string
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug("initial tz=" + event.start.tz);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug("corrected TZ=" + tz);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug("corrected tz=" + event.start.tz);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug("defined offset=" + start_offset + " hours");
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug("ical offset=" + current_offset + " date=" + date);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug("net mins=" + (current_offset * 60 - x));
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug("adjusted date=" + event.start);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug("start date/time=" + es.toDate());
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug("start offset=" + start_offset);
|
||||
|
||||
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug("event date=" + mm.toDate());
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug("adjustHours=" + adjustHours);
|
||||
return adjustHours;
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
Log.debug("have entries ");
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
/**
|
||||
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
|
||||
* corresponding time-format 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} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
case 12: {
|
||||
return { longDateFormat: { LT: "h:mm A" } };
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
case 24: {
|
||||
return { longDateFormat: { LT: "HH:mm" } };
|
||||
}
|
||||
default: {
|
||||
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add an 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, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
Log.debug("\nevent=" + JSON.stringify(event));
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
if (wrapEvents === true) {
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
|
||||
// max - 1 to account for a space
|
||||
currentLine += `${word} `;
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarUtils.getTitleFromEvent(event);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.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 (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && 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);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
if (currentLine.length > 0) {
|
||||
temp += `${currentLine}<br>${word} `;
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
temp += `${word}<br>`;
|
||||
}
|
||||
currentLine = "";
|
||||
}
|
||||
}
|
||||
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// get the offset of today where we are processing
|
||||
// this will be the correction we need to apply
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// for full day events, the time might be off from RRULE/Luxon problem
|
||||
// get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if the offset is negative, east of GMT where the problem is
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
}
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return `${string.trim().slice(0, maxLength)}…`;
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug("duration=" + duration);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug("saving event =" + description);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
let entries = 0;
|
||||
let events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (config.includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > config.maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
return events;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
*
|
||||
* @param msTZName
|
||||
* @returns {*|null}
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
titleTransform: function (title, titleReplace) {
|
||||
let transformedTitle = title;
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
title = event.description;
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const 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;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param title
|
||||
* @param filter
|
||||
* @param useRegex
|
||||
* @param regexFlags
|
||||
* @returns {boolean|*}
|
||||
*/
|
||||
titleFilterApplies: 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);
|
||||
transformedTitle = transformedTitle.replace(needle, replacement);
|
||||
}
|
||||
return transformedTitle;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/* CalendarFetcher Tester
|
||||
* use this script with `node debug.js` to test the fetcher without the need
|
||||
* of starting the MagicMirror core. Adjust the values below to your desire.
|
||||
* of starting the MagicMirror² core. Adjust the values below to your desire.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
|
||||
@@ -26,11 +29,13 @@ const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maxi
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
console.log("------------------------------------------------------------");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.log("Fetcher error:");
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
fetcher.startFetch();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("logger");
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
@@ -19,13 +19,20 @@ module.exports = NodeHelper.create({
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
||||
} else if (notification === "FETCH_CALENDAR") {
|
||||
const key = payload.id + payload.url;
|
||||
if (typeof this.fetchers[key] === "undefined") {
|
||||
Log.error("Calendar Error. No fetcher exists with key: ", key);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_UNSPECIFIED" });
|
||||
return;
|
||||
}
|
||||
this.fetchers[key].startFetch();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a fetcher for a new url if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* @param {string} url The url of the calendar
|
||||
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
@@ -40,13 +47,14 @@ module.exports = NodeHelper.create({
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
Log.error("Calendar Error. Malformed calendar url: ", url, error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
@@ -55,16 +63,16 @@ module.exports = NodeHelper.create({
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
error_type
|
||||
});
|
||||
});
|
||||
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
Log.log(`Use existing calendarfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Module: Clock
|
||||
|
||||
The `clock` module is one of the default modules of the MagicMirror.
|
||||
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.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/clock.html).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global SunCalc */
|
||||
/* global SunCalc, formatTime */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Clock
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -12,21 +12,24 @@ Module.register("clock", {
|
||||
displayType: "digital", // options: digital, analog, both
|
||||
|
||||
timeFormat: config.timeFormat,
|
||||
timezone: null,
|
||||
|
||||
displaySeconds: true,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
clockBold: false,
|
||||
showDate: true,
|
||||
showTime: true,
|
||||
showWeek: false,
|
||||
dateFormat: "dddd, LL",
|
||||
sendNotifications: false,
|
||||
|
||||
/* specific to the analog clock */
|
||||
analogSize: "200px",
|
||||
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
|
||||
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
|
||||
analogShowDate: "top", // options: false, 'top', or 'bottom'
|
||||
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
|
||||
secondsColor: "#888888",
|
||||
timezone: null,
|
||||
|
||||
showSunTimes: false,
|
||||
showMoonTimes: false,
|
||||
@@ -43,18 +46,17 @@ Module.register("clock", {
|
||||
},
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Schedule update interval.
|
||||
var self = this;
|
||||
self.second = moment().second();
|
||||
self.minute = moment().minute();
|
||||
this.second = moment().second();
|
||||
this.minute = moment().minute();
|
||||
|
||||
// Calculate how many ms should pass until next update depending on if seconds is displayed or not
|
||||
var delayCalculator = function (reducedSeconds) {
|
||||
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
|
||||
const delayCalculator = (reducedSeconds) => {
|
||||
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
|
||||
|
||||
if (self.config.displaySeconds) {
|
||||
if (this.config.displaySeconds) {
|
||||
return 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
} else {
|
||||
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
@@ -62,50 +64,64 @@ Module.register("clock", {
|
||||
};
|
||||
|
||||
// A recursive timeout function instead of interval to avoid drifting
|
||||
var notificationTimer = function () {
|
||||
self.updateDom();
|
||||
const notificationTimer = () => {
|
||||
this.updateDom();
|
||||
|
||||
if (this.config.sendNotifications) {
|
||||
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (self.config.displaySeconds) {
|
||||
self.second = moment().second();
|
||||
if (self.second !== 0) {
|
||||
self.sendNotification("CLOCK_SECOND", self.second);
|
||||
if (this.config.displaySeconds) {
|
||||
this.second = moment().second();
|
||||
if (this.second !== 0) {
|
||||
this.sendNotification("CLOCK_SECOND", this.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
self.minute = moment().minute();
|
||||
self.sendNotification("CLOCK_MINUTE", self.minute);
|
||||
this.minute = moment().minute();
|
||||
this.sendNotification("CLOCK_MINUTE", this.minute);
|
||||
}
|
||||
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
};
|
||||
|
||||
//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(self.second));
|
||||
// Set the initial timeout with the amount of seconds elapsed as
|
||||
// reducedSeconds, so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(this.second));
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
},
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("clock-grid");
|
||||
|
||||
/************************************
|
||||
* Create wrappers for analog and digital clock
|
||||
*/
|
||||
const analogWrapper = document.createElement("div");
|
||||
analogWrapper.className = "clock-circle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
|
||||
/************************************
|
||||
* Create wrappers for DIGITAL clock
|
||||
*/
|
||||
const dateWrapper = document.createElement("div");
|
||||
const timeWrapper = document.createElement("div");
|
||||
const secondsWrapper = document.createElement("sup");
|
||||
const periodWrapper = document.createElement("span");
|
||||
const sunWrapper = document.createElement("div");
|
||||
const moonWrapper = document.createElement("div");
|
||||
const weekWrapper = document.createElement("div");
|
||||
|
||||
var dateWrapper = document.createElement("div");
|
||||
var timeWrapper = document.createElement("div");
|
||||
var secondsWrapper = document.createElement("sup");
|
||||
var periodWrapper = document.createElement("span");
|
||||
var sunWrapper = document.createElement("div");
|
||||
var moonWrapper = document.createElement("div");
|
||||
var weekWrapper = document.createElement("div");
|
||||
// Style Wrappers
|
||||
dateWrapper.className = "date normal medium";
|
||||
timeWrapper.className = "time bright large light";
|
||||
secondsWrapper.className = "dimmed";
|
||||
secondsWrapper.className = "seconds dimmed";
|
||||
sunWrapper.className = "sun dimmed small";
|
||||
moonWrapper.className = "moon dimmed small";
|
||||
weekWrapper.className = "week dimmed medium";
|
||||
@@ -114,30 +130,29 @@ Module.register("clock", {
|
||||
// The moment().format("h") method has a bug on the Raspberry Pi.
|
||||
// So we need to generate the timestring manually.
|
||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||
var timeString;
|
||||
var now = moment();
|
||||
this.lastDisplayedMinute = now.minute();
|
||||
let timeString;
|
||||
const now = moment();
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
|
||||
var hourSymbol = "HH";
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
if (this.config.clockBold === true) {
|
||||
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
|
||||
if (this.config.clockBold) {
|
||||
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
|
||||
} else {
|
||||
timeString = now.format(hourSymbol + ":mm");
|
||||
timeString = now.format(`${hourSymbol}:mm`);
|
||||
}
|
||||
|
||||
if (this.config.showDate) {
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
}
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
}
|
||||
|
||||
if (this.config.displayType !== "analog" && this.config.showTime) {
|
||||
timeWrapper.innerHTML = timeString;
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
if (this.config.showPeriodUpper) {
|
||||
@@ -151,26 +166,16 @@ Module.register("clock", {
|
||||
if (this.config.showPeriod && this.config.timeFormat !== 24) {
|
||||
timeWrapper.appendChild(periodWrapper);
|
||||
}
|
||||
digitalWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time according to the config
|
||||
*
|
||||
* @param {object} config The config of the module
|
||||
* @param {object} time time to format
|
||||
* @returns {string} The formatted time string
|
||||
/****************************************************************
|
||||
* Create wrappers for Sun Times, only if specified in config
|
||||
*/
|
||||
function formatTime(config, time) {
|
||||
var formatString = hourSymbol + ":mm";
|
||||
if (config.showPeriod && config.timeFormat !== 24) {
|
||||
formatString += config.showPeriodUpper ? "A" : "a";
|
||||
}
|
||||
return moment(time).format(formatString);
|
||||
}
|
||||
|
||||
if (this.config.showSunTimes) {
|
||||
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
|
||||
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
|
||||
var nextEvent;
|
||||
let nextEvent;
|
||||
if (now.isBefore(sunTimes.sunrise)) {
|
||||
nextEvent = sunTimes.sunrise;
|
||||
} else if (now.isBefore(sunTimes.sunset)) {
|
||||
@@ -180,25 +185,22 @@ Module.register("clock", {
|
||||
nextEvent = tomorrowSunTimes.sunrise;
|
||||
}
|
||||
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
|
||||
const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m";
|
||||
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
|
||||
sunWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
||||
untilNextEventString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunrise) +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for Moon Times, only if specified in config
|
||||
*/
|
||||
if (this.config.showMoonTimes) {
|
||||
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
|
||||
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
|
||||
const moonRise = moonTimes.rise;
|
||||
var moonSet;
|
||||
let moonSet;
|
||||
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
|
||||
moonSet = moonTimes.set;
|
||||
} else {
|
||||
@@ -206,25 +208,22 @@ Module.register("clock", {
|
||||
moonSet = nextMoonTimes.set;
|
||||
}
|
||||
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
|
||||
const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%";
|
||||
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
|
||||
moonWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fa fa-moon-o" aria-hidden="true"></i> ' +
|
||||
illuminatedFractionString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
(moonRise ? formatTime(this.config, moonRise) : "...") +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
(moonSet ? formatTime(this.config, moonSet) : "...") +
|
||||
"</span>";
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
}
|
||||
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
digitalWrapper.appendChild(weekWrapper);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for ANALOG clock, only if specified in config
|
||||
*/
|
||||
|
||||
if (this.config.displayType !== "digital") {
|
||||
// If it isn't 'digital', then an 'analog' clock was also requested
|
||||
|
||||
@@ -232,126 +231,73 @@ Module.register("clock", {
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
var second = now.seconds() * 6,
|
||||
const second = now.seconds() * 6,
|
||||
minute = now.minute() * 6 + second / 60,
|
||||
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
|
||||
|
||||
// Create wrappers
|
||||
var clockCircle = document.createElement("div");
|
||||
clockCircle.className = "clockCircle";
|
||||
clockCircle.style.width = this.config.analogSize;
|
||||
clockCircle.style.height = this.config.analogSize;
|
||||
analogWrapper.style.width = this.config.analogSize;
|
||||
analogWrapper.style.height = this.config.analogSize;
|
||||
|
||||
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
|
||||
clockCircle.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
|
||||
clockCircle.style.backgroundSize = "100%";
|
||||
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
|
||||
analogWrapper.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
// 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
|
||||
// analogWrapper.style.border = "1px solid black";
|
||||
analogWrapper.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";
|
||||
analogWrapper.style.border = "2px solid white";
|
||||
}
|
||||
var clockFace = document.createElement("div");
|
||||
clockFace.className = "clockFace";
|
||||
const clockFace = document.createElement("div");
|
||||
clockFace.className = "clock-face";
|
||||
|
||||
var clockHour = document.createElement("div");
|
||||
clockHour.id = "clockHour";
|
||||
clockHour.style.transform = "rotate(" + hour + "deg)";
|
||||
clockHour.className = "clockHour";
|
||||
var clockMinute = document.createElement("div");
|
||||
clockMinute.id = "clockMinute";
|
||||
clockMinute.style.transform = "rotate(" + minute + "deg)";
|
||||
clockMinute.className = "clockMinute";
|
||||
const clockHour = document.createElement("div");
|
||||
clockHour.id = "clock-hour";
|
||||
clockHour.style.transform = `rotate(${hour}deg)`;
|
||||
clockHour.className = "clock-hour";
|
||||
const clockMinute = document.createElement("div");
|
||||
clockMinute.id = "clock-minute";
|
||||
clockMinute.style.transform = `rotate(${minute}deg)`;
|
||||
clockMinute.className = "clock-minute";
|
||||
|
||||
// Combine analog wrappers
|
||||
clockFace.appendChild(clockHour);
|
||||
clockFace.appendChild(clockMinute);
|
||||
|
||||
if (this.config.displaySeconds) {
|
||||
var clockSecond = document.createElement("div");
|
||||
clockSecond.id = "clockSecond";
|
||||
clockSecond.style.transform = "rotate(" + second + "deg)";
|
||||
clockSecond.className = "clockSecond";
|
||||
const clockSecond = document.createElement("div");
|
||||
clockSecond.id = "clock-second";
|
||||
clockSecond.style.transform = `rotate(${second}deg)`;
|
||||
clockSecond.className = "clock-second";
|
||||
clockSecond.style.backgroundColor = this.config.secondsColor;
|
||||
clockFace.appendChild(clockSecond);
|
||||
}
|
||||
clockCircle.appendChild(clockFace);
|
||||
analogWrapper.appendChild(clockFace);
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Combine wrappers, check for .displayType
|
||||
* Update placement, respect old analogShowDate even if it's not needed anymore
|
||||
*/
|
||||
|
||||
if (this.config.displayType === "digital") {
|
||||
// Display only a digital clock
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(timeWrapper);
|
||||
wrapper.appendChild(sunWrapper);
|
||||
wrapper.appendChild(moonWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
} else if (this.config.displayType === "analog") {
|
||||
if (this.config.displayType === "analog") {
|
||||
// Display only an analog clock
|
||||
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.style.paddingBottom = "15px";
|
||||
} else {
|
||||
dateWrapper.style.paddingBottom = "15px";
|
||||
}
|
||||
|
||||
if (this.config.analogShowDate === "top") {
|
||||
if (this.config.showDate) {
|
||||
// Add date to the analog clock
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
wrapper.appendChild(clockCircle);
|
||||
} else if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.appendChild(clockCircle);
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
} else {
|
||||
wrapper.appendChild(clockCircle);
|
||||
}
|
||||
} else {
|
||||
// Both clocks have been configured, check position
|
||||
var placement = this.config.analogPlacement;
|
||||
|
||||
var analogWrapper = document.createElement("div");
|
||||
analogWrapper.id = "analog";
|
||||
analogWrapper.style.cssFloat = "none";
|
||||
analogWrapper.appendChild(clockCircle);
|
||||
|
||||
var digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.id = "digital";
|
||||
digitalWrapper.style.cssFloat = "none";
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
digitalWrapper.appendChild(timeWrapper);
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
digitalWrapper.appendChild(weekWrapper);
|
||||
|
||||
var appendClocks = function (condition, pos1, pos2) {
|
||||
var padding = [0, 0, 0, 0];
|
||||
padding[placement === condition ? pos1 : pos2] = "20px";
|
||||
analogWrapper.style.padding = padding.join(" ");
|
||||
if (placement === condition) {
|
||||
if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.classList.add("clock-grid-bottom");
|
||||
} else if (this.config.analogShowDate === "top") {
|
||||
wrapper.classList.add("clock-grid-top");
|
||||
}
|
||||
wrapper.appendChild(analogWrapper);
|
||||
} else if (this.config.displayType === "digital") {
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
} else if (this.config.displayType === "both") {
|
||||
wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
} else {
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
wrapper.appendChild(analogWrapper);
|
||||
}
|
||||
};
|
||||
|
||||
if (placement === "left" || placement === "right") {
|
||||
digitalWrapper.style.display = "inline-block";
|
||||
digitalWrapper.style.verticalAlign = "top";
|
||||
analogWrapper.style.display = "inline-block";
|
||||
|
||||
appendClocks("left", 1, 3);
|
||||
} else {
|
||||
digitalWrapper.style.textAlign = "center";
|
||||
|
||||
appendClocks("top", 2, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the wrapper to the dom.
|
||||
|
||||
@@ -1,42 +1,63 @@
|
||||
.clockCircle {
|
||||
margin: 0 auto;
|
||||
.clock-grid {
|
||||
display: inline-flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.clock-grid-left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.clock-grid-right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.clock-grid-top {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clock-grid-bottom {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.clock-circle {
|
||||
place-self: center;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.clockFace {
|
||||
.clock-face {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.clockFace::after {
|
||||
.clock-face::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: -3px 0 0 -3px;
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
border-radius: 3px;
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
.clockHour {
|
||||
.clock-hour {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */
|
||||
margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */
|
||||
padding: 2px 0 2px 25%; /* indicator length & thickness */
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.clockMinute {
|
||||
.clock-minute {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
@@ -44,12 +65,12 @@
|
||||
left: 50%;
|
||||
margin: -35% -2px 0; /* numbers must match negative length & thickness */
|
||||
padding: 35% 2px 0; /* indicator length & thickness */
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
transform-origin: 50% 100%;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.clockSecond {
|
||||
.clock-second {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
@@ -57,7 +78,7 @@
|
||||
left: 50%;
|
||||
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
|
||||
padding: 38% 1px 0 0; /* indicator length & thickness */
|
||||
background: #888;
|
||||
background: var(--color-text);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Module: Compliments
|
||||
|
||||
The `compliments` module is one of the default modules of the MagicMirror.
|
||||
The `compliments` module is one of the default modules of the MagicMirror².
|
||||
This module displays a random compliment.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/compliments.html).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Compliments
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -21,8 +21,7 @@ Module.register("compliments", {
|
||||
morningEndTime: 12,
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true,
|
||||
mockDate: null
|
||||
random: true
|
||||
},
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
@@ -34,42 +33,38 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start: async function () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
this.lastComplimentIndex = -1;
|
||||
|
||||
var self = this;
|
||||
if (this.config.remoteFile !== null) {
|
||||
this.complimentFile(function (response) {
|
||||
self.config.compliments = JSON.parse(response);
|
||||
self.updateDom();
|
||||
});
|
||||
const response = await this.loadComplimentFile();
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
}
|
||||
|
||||
// Schedule update timer.
|
||||
setInterval(function () {
|
||||
self.updateDom(self.config.fadeSpeed);
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
/* randomIndex(compliments)
|
||||
/**
|
||||
* Generate a random index for a list of compliments.
|
||||
*
|
||||
* argument compliments Array<String> - Array with compliments.
|
||||
*
|
||||
* return Number - Random index.
|
||||
* @param {string[]} compliments Array with compliments.
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
randomIndex: function (compliments) {
|
||||
if (compliments.length === 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var generate = function () {
|
||||
const generate = function () {
|
||||
return Math.floor(Math.random() * compliments.length);
|
||||
};
|
||||
|
||||
var complimentIndex = generate();
|
||||
let complimentIndex = generate();
|
||||
|
||||
while (complimentIndex === this.lastComplimentIndex) {
|
||||
complimentIndex = generate();
|
||||
@@ -80,70 +75,62 @@ Module.register("compliments", {
|
||||
return complimentIndex;
|
||||
},
|
||||
|
||||
/* complimentArray()
|
||||
/**
|
||||
* Retrieve an array of compliments for the time of the day.
|
||||
*
|
||||
* return compliments Array<String> - Array with compliments for the time of the day.
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray: function () {
|
||||
var hour = moment().hour();
|
||||
var date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
|
||||
var compliments;
|
||||
const hour = moment().hour();
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
|
||||
// Add time of day compliments
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||
compliments = this.config.compliments.morning.slice(0);
|
||||
compliments = [...this.config.compliments.morning];
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||
compliments = this.config.compliments.afternoon.slice(0);
|
||||
compliments = [...this.config.compliments.afternoon];
|
||||
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
||||
compliments = this.config.compliments.evening.slice(0);
|
||||
}
|
||||
|
||||
if (typeof compliments === "undefined") {
|
||||
compliments = new Array();
|
||||
compliments = [...this.config.compliments.evening];
|
||||
}
|
||||
|
||||
// Add compliments based on weather
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
}
|
||||
|
||||
compliments.push.apply(compliments, this.config.compliments.anytime);
|
||||
// Add compliments for anytime
|
||||
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
for (var entry in this.config.compliments) {
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
compliments.push.apply(compliments, this.config.compliments[entry]);
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
return compliments;
|
||||
},
|
||||
|
||||
/* complimentFile(callback)
|
||||
/**
|
||||
* Retrieve a file from the local filesystem
|
||||
* @returns {Promise} Resolved when the file is loaded
|
||||
*/
|
||||
complimentFile: function (callback) {
|
||||
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", path, true);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState === 4 && xobj.status === 200) {
|
||||
callback(xobj.responseText);
|
||||
}
|
||||
};
|
||||
xobj.send(null);
|
||||
loadComplimentFile: async function () {
|
||||
const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
||||
url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
||||
const response = await fetch(url);
|
||||
return await response.text();
|
||||
},
|
||||
|
||||
/* complimentArray()
|
||||
/**
|
||||
* Retrieve a random compliment.
|
||||
*
|
||||
* return compliment string - A compliment.
|
||||
* @returns {string} a compliment
|
||||
*/
|
||||
randomCompliment: function () {
|
||||
getRandomCompliment: function () {
|
||||
// get the current time of day compliments list
|
||||
var compliments = this.complimentArray();
|
||||
const compliments = this.complimentArray();
|
||||
// variable for index to next message to display
|
||||
let index = 0;
|
||||
let index;
|
||||
// are we randomizing
|
||||
if (this.config.random) {
|
||||
// yes
|
||||
@@ -159,37 +146,36 @@ Module.register("compliments", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||
// get the compliment text
|
||||
var complimentText = this.randomCompliment();
|
||||
const complimentText = this.getRandomCompliment();
|
||||
// split it into parts on newline text
|
||||
var parts = complimentText.split("\n");
|
||||
// create a span to hold it all
|
||||
var compliment = document.createElement("span");
|
||||
const parts = complimentText.split("\n");
|
||||
// create a span to hold the compliment
|
||||
const compliment = document.createElement("span");
|
||||
// process all the parts of the compliment text
|
||||
for (var part of parts) {
|
||||
for (const part of parts) {
|
||||
if (part !== "") {
|
||||
// create a text element for each part
|
||||
compliment.appendChild(document.createTextNode(part));
|
||||
// add a break `
|
||||
// add a break
|
||||
compliment.appendChild(document.createElement("BR"));
|
||||
}
|
||||
}
|
||||
// only add compliment to wrapper if there is actual text in there
|
||||
if (compliment.children.length > 0) {
|
||||
// remove the last break
|
||||
compliment.lastElementChild.remove();
|
||||
wrapper.appendChild(compliment);
|
||||
|
||||
}
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
// From data currentweather set weather type
|
||||
setCurrentWeatherType: function (type) {
|
||||
this.currentWeatherType = type;
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "CURRENTWEATHER_TYPE") {
|
||||
this.setCurrentWeatherType(payload.type);
|
||||
this.currentWeatherType = payload.type;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Module: Current Weather
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
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.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/currentweather.html).
|
||||
@@ -1,15 +0,0 @@
|
||||
.currentweather .weathericon,
|
||||
.currentweather .fa-home {
|
||||
font-size: 75%;
|
||||
line-height: 65px;
|
||||
display: inline-block;
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.currentweather .humidityIcon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.currentweather .humidity-padding {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
/* Magic Mirror
|
||||
* Module: CurrentWeather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("currentweather", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
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,
|
||||
useKMPHwind: false,
|
||||
lang: config.language,
|
||||
decimalSymbol: ".",
|
||||
showHumidity: false,
|
||||
showSun: true,
|
||||
degreeLabel: false,
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
showFeelsLike: true,
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
useLocationAsHeader: false,
|
||||
|
||||
calendarClass: "calendar",
|
||||
tableClass: "large",
|
||||
|
||||
onlyTemp: false,
|
||||
hideTemp: false,
|
||||
roundTemp: false,
|
||||
|
||||
iconTable: {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
// 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.
|
||||
fetchedLocationName: "",
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
return ["weather-icons.css", "currentweather.css"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
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 your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
this.windDeg = null;
|
||||
this.sunriseSunsetTime = null;
|
||||
this.sunriseSunsetIcon = null;
|
||||
this.temperature = null;
|
||||
this.indoorTemperature = null;
|
||||
this.indoorHumidity = null;
|
||||
this.weatherType = null;
|
||||
this.feelsLike = null;
|
||||
this.loaded = false;
|
||||
this.scheduleUpdate(this.config.initialLoadDelay);
|
||||
},
|
||||
|
||||
// add extra information of current weather
|
||||
// windDirection, humidity, sunrise and sunset
|
||||
addExtraInfoWeather: function (wrapper) {
|
||||
var small = document.createElement("div");
|
||||
small.className = "normal medium";
|
||||
|
||||
var windIcon = document.createElement("span");
|
||||
windIcon.className = "wi wi-strong-wind dimmed";
|
||||
small.appendChild(windIcon);
|
||||
|
||||
var windSpeed = document.createElement("span");
|
||||
windSpeed.innerHTML = " " + this.windSpeed;
|
||||
small.appendChild(windSpeed);
|
||||
|
||||
if (this.config.showWindDirection) {
|
||||
var windDirection = document.createElement("sup");
|
||||
if (this.config.showWindDirectionAsArrow) {
|
||||
if (this.windDeg !== null) {
|
||||
windDirection.innerHTML = ' <i class="fa fa-long-arrow-down" style="transform:rotate(' + this.windDeg + 'deg);"></i> ';
|
||||
}
|
||||
} else {
|
||||
windDirection.innerHTML = " " + this.translate(this.windDirection);
|
||||
}
|
||||
small.appendChild(windDirection);
|
||||
}
|
||||
var spacer = document.createElement("span");
|
||||
spacer.innerHTML = " ";
|
||||
small.appendChild(spacer);
|
||||
|
||||
if (this.config.showHumidity) {
|
||||
var humidity = document.createElement("span");
|
||||
humidity.innerHTML = this.humidity;
|
||||
|
||||
var supspacer = document.createElement("sup");
|
||||
supspacer.innerHTML = " ";
|
||||
|
||||
var humidityIcon = document.createElement("sup");
|
||||
humidityIcon.className = "wi wi-humidity humidityIcon";
|
||||
humidityIcon.innerHTML = " ";
|
||||
|
||||
small.appendChild(humidity);
|
||||
small.appendChild(supspacer);
|
||||
small.appendChild(humidityIcon);
|
||||
}
|
||||
|
||||
if (this.config.showSun) {
|
||||
var sunriseSunsetIcon = document.createElement("span");
|
||||
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
|
||||
small.appendChild(sunriseSunsetIcon);
|
||||
|
||||
var sunriseSunsetTime = document.createElement("span");
|
||||
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
|
||||
small.appendChild(sunriseSunsetTime);
|
||||
}
|
||||
|
||||
wrapper.appendChild(small);
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (this.config.appid === "") {
|
||||
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
wrapper.innerHTML = this.translate("LOADING");
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (this.config.onlyTemp === false) {
|
||||
this.addExtraInfoWeather(wrapper);
|
||||
}
|
||||
|
||||
var large = document.createElement("div");
|
||||
large.className = "light";
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if (this.config.degreeLabel) {
|
||||
switch (this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel += "K";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.decimalSymbol === "") {
|
||||
this.config.decimalSymbol = ".";
|
||||
}
|
||||
|
||||
if (this.config.hideTemp === false) {
|
||||
var weatherIcon = document.createElement("span");
|
||||
weatherIcon.className = "wi weathericon wi-" + this.weatherType;
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
temperature.className = "bright";
|
||||
temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(temperature);
|
||||
}
|
||||
|
||||
if (this.config.showIndoorTemperature && this.indoorTemperature) {
|
||||
var indoorIcon = document.createElement("span");
|
||||
indoorIcon.className = "fa fa-home";
|
||||
large.appendChild(indoorIcon);
|
||||
|
||||
var indoorTemperatureElem = document.createElement("span");
|
||||
indoorTemperatureElem.className = "bright";
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(indoorTemperatureElem);
|
||||
}
|
||||
|
||||
if (this.config.showIndoorHumidity && this.indoorHumidity) {
|
||||
var indoorHumidityIcon = document.createElement("span");
|
||||
indoorHumidityIcon.className = "fa fa-tint";
|
||||
large.appendChild(indoorHumidityIcon);
|
||||
|
||||
var indoorHumidityElem = document.createElement("span");
|
||||
indoorHumidityElem.className = "bright";
|
||||
indoorHumidityElem.innerHTML = " " + this.indoorHumidity + "%";
|
||||
large.appendChild(indoorHumidityElem);
|
||||
}
|
||||
|
||||
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", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.useLocationAsHeader && this.config.location !== false) {
|
||||
return this.config.location;
|
||||
}
|
||||
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
this.hide(0, { lockString: this.identifier });
|
||||
}
|
||||
}
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
var senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (notification === "INDOOR_TEMPERATURE") {
|
||||
this.indoorTemperature = this.roundValue(payload);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
},
|
||||
|
||||
/* updateWeather(compliments)
|
||||
* Requests new data from openweather.org.
|
||||
* Calls processWeather on succesfull response.
|
||||
*/
|
||||
updateWeather: function () {
|
||||
if (this.config.appid === "") {
|
||||
Log.error("CurrentWeather: APPID not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + this.getParams();
|
||||
var self = this;
|
||||
var retry = true;
|
||||
|
||||
var weatherRequest = new XMLHttpRequest();
|
||||
weatherRequest.open("GET", url, true);
|
||||
weatherRequest.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
self.processWeather(JSON.parse(this.response));
|
||||
} else if (this.status === 401) {
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
|
||||
Log.error(self.name + ": Incorrect APPID.");
|
||||
retry = true;
|
||||
} else {
|
||||
Log.error(self.name + ": Could not load weather.");
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
self.scheduleUpdate(self.loaded ? -1 : self.config.retryDelay);
|
||||
}
|
||||
}
|
||||
};
|
||||
weatherRequest.send();
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams: function () {
|
||||
var 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.appid;
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
/* processWeather(data)
|
||||
* Uses the received data to set the various values.
|
||||
*
|
||||
* argument data object - Weather information received form openweather.org.
|
||||
*/
|
||||
processWeather: function (data) {
|
||||
if (!data || !data.main || typeof data.main.temp === "undefined") {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.humidity = parseFloat(data.main.humidity);
|
||||
this.temperature = this.roundValue(data.main.temp);
|
||||
this.fetchedLocationName = data.name;
|
||||
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":
|
||||
tempInF = 1.8 * (this.temperature - 273.15) + 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":
|
||||
this.feelsLike = (windChillInC + 273.15).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];
|
||||
|
||||
var now = new Date();
|
||||
var sunrise = new Date(data.sys.sunrise * 1000);
|
||||
var sunset = new Date(data.sys.sunset * 1000);
|
||||
|
||||
// The moment().format('h') method has a bug on the Raspberry Pi.
|
||||
// So we need to generate the timestring manually.
|
||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||
var sunriseSunsetDateObject = sunrise < now && sunset > now ? sunset : sunrise;
|
||||
var timeString = moment(sunriseSunsetDateObject).format("HH:mm");
|
||||
if (this.config.timeFormat !== 24) {
|
||||
//var hours = sunriseSunsetDateObject.getHours() % 12 || 12;
|
||||
if (this.config.showPeriod) {
|
||||
if (this.config.showPeriodUpper) {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm A');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm A");
|
||||
} else {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm a');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
this.sunriseSunsetTime = timeString;
|
||||
this.sunriseSunsetIcon = sunrise < now && sunset > now ? "wi-sunset" : "wi-sunrise";
|
||||
|
||||
this.show(this.config.animationSpeed, { lockString: this.identifier });
|
||||
this.loaded = true;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
this.sendNotification("CURRENTWEATHER_DATA", { data: data });
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.config.iconTable[data.weather[0].icon].replace("-", "_") });
|
||||
},
|
||||
|
||||
/* scheduleUpdate()
|
||||
* Schedule next update.
|
||||
*
|
||||
* argument delay number - Milliseconds before next update. If empty, this.config.updateInterval is used.
|
||||
*/
|
||||
scheduleUpdate: function (delay) {
|
||||
var nextLoad = this.config.updateInterval;
|
||||
if (typeof delay !== "undefined" && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self.updateWeather();
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
/* ms2Beaufort(ms)
|
||||
* Converts m2 to beaufort (windspeed).
|
||||
*
|
||||
* see:
|
||||
* https://www.spc.noaa.gov/faq/tornado/beaufort.html
|
||||
* https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
|
||||
*
|
||||
* argument ms number - Windspeed in m/s.
|
||||
*
|
||||
* return number - Windspeed in beaufort.
|
||||
*/
|
||||
ms2Beaufort: function (ms) {
|
||||
var kmh = (ms * 60 * 60) / 1000;
|
||||
var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (var beaufort in speeds) {
|
||||
var speed = speeds[beaufort];
|
||||
if (speed > kmh) {
|
||||
return beaufort;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
},
|
||||
|
||||
deg2Cardinal: function (deg) {
|
||||
if (deg > 11.25 && deg <= 33.75) {
|
||||
return "NNE";
|
||||
} else if (deg > 33.75 && deg <= 56.25) {
|
||||
return "NE";
|
||||
} else if (deg > 56.25 && deg <= 78.75) {
|
||||
return "ENE";
|
||||
} else if (deg > 78.75 && deg <= 101.25) {
|
||||
return "E";
|
||||
} else if (deg > 101.25 && deg <= 123.75) {
|
||||
return "ESE";
|
||||
} else if (deg > 123.75 && deg <= 146.25) {
|
||||
return "SE";
|
||||
} else if (deg > 146.25 && deg <= 168.75) {
|
||||
return "SSE";
|
||||
} else if (deg > 168.75 && deg <= 191.25) {
|
||||
return "S";
|
||||
} else if (deg > 191.25 && deg <= 213.75) {
|
||||
return "SSW";
|
||||
} else if (deg > 213.75 && deg <= 236.25) {
|
||||
return "SW";
|
||||
} else if (deg > 236.25 && deg <= 258.75) {
|
||||
return "WSW";
|
||||
} else if (deg > 258.75 && deg <= 281.25) {
|
||||
return "W";
|
||||
} else if (deg > 281.25 && deg <= 303.75) {
|
||||
return "WNW";
|
||||
} else if (deg > 303.75 && deg <= 326.25) {
|
||||
return "NW";
|
||||
} else if (deg > 326.25 && deg <= 348.75) {
|
||||
return "NNW";
|
||||
} else {
|
||||
return "N";
|
||||
}
|
||||
},
|
||||
|
||||
/* function(temperature)
|
||||
* Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
|
||||
*
|
||||
* argument temperature number - Temperature.
|
||||
*
|
||||
* return string - Rounded Temperature.
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
}
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
||||
@@ -1,13 +1,10 @@
|
||||
/* Magic Mirror
|
||||
* Default Modules List
|
||||
/* MagicMirror² Default Modules List
|
||||
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
// Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
|
||||
var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Module: Hello World
|
||||
|
||||
The `helloworld` module is one of the default modules of the MagicMirror. It is a simple way to display a static text on the mirror.
|
||||
The `helloworld` module is one of the default modules of the MagicMirror². It is a simple way to display a static text on the mirror.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/helloworld.html).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: HelloWorld
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Module: News Feed
|
||||
|
||||
The `newsfeed` module is one of the default modules of the MagicMirror.
|
||||
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.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/newsfeed.html).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
iframe.newsfeed-fullarticle {
|
||||
width: 100vw;
|
||||
|
||||
/* very large height value to allow scrolling */
|
||||
height: 3000px;
|
||||
top: 0;
|
||||
@@ -12,3 +13,12 @@ iframe.newsfeed-fullarticle {
|
||||
bottom: inherit;
|
||||
top: -90px;
|
||||
}
|
||||
|
||||
.newsfeed-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.newsfeed-list li {
|
||||
text-align: justify;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: NewsFeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -14,11 +14,13 @@ Module.register("newsfeed", {
|
||||
encoding: "UTF-8" //ISO-8859-1
|
||||
}
|
||||
],
|
||||
showAsList: false,
|
||||
showSourceTitle: true,
|
||||
showPublishDate: true,
|
||||
broadcastNewsFeeds: true,
|
||||
broadcastNewsUpdates: true,
|
||||
showDescription: false,
|
||||
showTitleAsUrl: false,
|
||||
wrapTitle: true,
|
||||
wrapDescription: true,
|
||||
truncDescription: true,
|
||||
@@ -36,7 +38,16 @@ Module.register("newsfeed", {
|
||||
endTags: [],
|
||||
prohibitedWords: [],
|
||||
scrollLength: 500,
|
||||
logFeedWarnings: false
|
||||
logFeedWarnings: false,
|
||||
dangerouslyDisableAutoEscaping: false
|
||||
},
|
||||
|
||||
getUrlPrefix: function (item) {
|
||||
if (item.useCorsProxy) {
|
||||
return `${location.protocol}//${location.host}/cors?url=`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -59,7 +70,7 @@ Module.register("newsfeed", {
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
@@ -89,8 +100,8 @@ Module.register("newsfeed", {
|
||||
|
||||
this.loaded = true;
|
||||
this.error = null;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
this.error = `Incorrect url: ${payload.url}`;
|
||||
} else if (notification === "NEWSFEED_ERROR") {
|
||||
this.error = this.translate(payload.error_type);
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
},
|
||||
@@ -120,7 +131,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
return {
|
||||
loaded: false
|
||||
empty: true
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
@@ -128,6 +139,10 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
const item = this.newsItems[this.activeItem];
|
||||
const items = this.newsItems.map(function (item) {
|
||||
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
||||
return item;
|
||||
});
|
||||
|
||||
return {
|
||||
loaded: true,
|
||||
@@ -135,12 +150,19 @@ Module.register("newsfeed", {
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
description: item.description
|
||||
url: this.getUrlPrefix(item) + item.url,
|
||||
description: item.description,
|
||||
items: items
|
||||
};
|
||||
},
|
||||
|
||||
getActiveItemURL: function () {
|
||||
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||
const item = this.newsItems[this.activeItem];
|
||||
if (item) {
|
||||
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -157,7 +179,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Generate an ordered list of items for this configured module.
|
||||
*
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
@@ -178,21 +199,21 @@ Module.register("newsfeed", {
|
||||
const dateB = new Date(b.pubdate);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
if (this.config.maxNewsItems > 0) {
|
||||
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
||||
}
|
||||
|
||||
if (this.config.prohibitedWords.length > 0) {
|
||||
newsItems = newsItems.filter(function (value) {
|
||||
newsItems = newsItems.filter(function (item) {
|
||||
for (let word of this.config.prohibitedWords) {
|
||||
if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, this);
|
||||
}
|
||||
|
||||
newsItems.forEach((item) => {
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
@@ -214,7 +235,6 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.title.slice(-endTag.length) === endTag) {
|
||||
@@ -251,7 +271,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Check if this module is configured to show this feed.
|
||||
*
|
||||
* @param {string} feedUrl Url of the feed to check.
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
@@ -266,7 +285,6 @@ Module.register("newsfeed", {
|
||||
|
||||
/**
|
||||
* Returns title for the specific feed url.
|
||||
*
|
||||
* @param {string} feedUrl Url of the feed
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
@@ -290,6 +308,9 @@ Module.register("newsfeed", {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
|
||||
// #2638 Clear timer if it already exists
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
@@ -322,7 +343,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||
this.activeItem--;
|
||||
@@ -330,7 +351,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = this.newsItems.length - 1;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
// if "more details" is received the first time: show article summary, on second time show full article
|
||||
@@ -339,8 +360,8 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(this.name + " - scrolling down");
|
||||
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(`${this.name} - scrolling down`);
|
||||
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
@@ -348,12 +369,12 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.debug(this.name + " - scrolling up");
|
||||
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(`${this.name} - scrolling up`);
|
||||
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
||||
}
|
||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.debug(this.name + " - showing only article titles again");
|
||||
Log.debug(`${this.name} - showing only article titles again`);
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||
if (this.config.showFullArticle) {
|
||||
@@ -382,7 +403,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
||||
this.updateDom(100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,32 +1,89 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% endmacro %}
|
||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ title }}
|
||||
{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("logger");
|
||||
|
||||
const stream = require("stream");
|
||||
const FeedMe = require("feedme");
|
||||
const fetch = require("node-fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
const { htmlToText } = require("html-to-text");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
*
|
||||
* @param {string} url URL of the news feed.
|
||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||
* @param {string} encoding Encoding of the feed.
|
||||
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
||||
* @param {boolean} useCorsProxy If true cors proxy is used for article url's.
|
||||
* @class
|
||||
*/
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
let reloadIntervalMS = reloadInterval;
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let itemsReceivedCallback = function () {};
|
||||
|
||||
if (reloadInterval < 1000) {
|
||||
reloadInterval = 1000;
|
||||
if (reloadIntervalMS < 1000) {
|
||||
reloadIntervalMS = 1000;
|
||||
}
|
||||
|
||||
/* private methods */
|
||||
@@ -50,25 +54,27 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
if (title && pubdate) {
|
||||
const regex = /(<([^>]+)>)/gi;
|
||||
description = description.toString().replace(regex, "");
|
||||
// Convert HTML entities, codes and tag
|
||||
description = htmlToText(description, { wordwrap: false });
|
||||
|
||||
items.push({
|
||||
title: title,
|
||||
description: description,
|
||||
pubdate: pubdate,
|
||||
url: url
|
||||
url: url,
|
||||
useCorsProxy: useCorsProxy
|
||||
});
|
||||
} else if (logFeedWarnings) {
|
||||
Log.warn("Can't parse feed item:");
|
||||
Log.warn(item);
|
||||
Log.warn("Title: " + title);
|
||||
Log.warn("Description: " + description);
|
||||
Log.warn("Pubdate: " + pubdate);
|
||||
Log.warn(`Title: ${title}`);
|
||||
Log.warn(`Description: ${description}`);
|
||||
Log.warn(`Pubdate: ${pubdate}`);
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", () => {
|
||||
this.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("error", (error) => {
|
||||
@@ -76,20 +82,45 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
||||
parser.on("finish", () => {
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("ttl", (minutes) => {
|
||||
try {
|
||||
// 86400000 = 24 hours is mentioned in the docs as maximum value:
|
||||
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
|
||||
if (ttlms > reloadIntervalMS) {
|
||||
reloadIntervalMS = ttlms;
|
||||
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
|
||||
fetch(url, { headers: headers })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => {
|
||||
let nodeStream;
|
||||
if (response.body instanceof stream.Readable) {
|
||||
nodeStream = response.body;
|
||||
} else {
|
||||
nodeStream = stream.Readable.fromWeb(response.body);
|
||||
}
|
||||
nodeStream.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.then((res) => {
|
||||
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -100,19 +131,18 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadInterval);
|
||||
}, reloadIntervalMS);
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
* Update the reload interval, but only if we need to increase the speed.
|
||||
*
|
||||
* @param {number} interval Interval for the update in milliseconds.
|
||||
*/
|
||||
this.setReloadInterval = function (interval) {
|
||||
if (interval > 1000 && interval < reloadInterval) {
|
||||
reloadInterval = interval;
|
||||
if (interval > 1000 && interval < reloadIntervalMS) {
|
||||
reloadIntervalMS = interval;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,7 +161,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
|
||||
return;
|
||||
}
|
||||
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
|
||||
Log.info(`Newsfeed-Fetcher: Broadcasting ${items.length} items.`);
|
||||
itemsReceivedCallback(this);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -6,13 +6,13 @@
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher.js");
|
||||
const Log = require("logger");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
@@ -26,41 +26,44 @@ module.exports = NodeHelper.create({
|
||||
/**
|
||||
* Creates a fetcher for a new feed if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* @param {object} feed The feed object.
|
||||
* @param {object} config The configuration object.
|
||||
* @param {object} feed The feed object
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
createFetcher: function (feed, config) {
|
||||
const url = feed.url || "";
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
let useCorsProxy = feed.useCorsProxy;
|
||||
if (useCorsProxy === undefined) useCorsProxy = true;
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { url: url });
|
||||
Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
Log.log(`Create new newsfetcher for url: ${url} - Interval: ${reloadInterval}`);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy);
|
||||
|
||||
fetcher.onReceive(() => {
|
||||
this.broadcastFeeds();
|
||||
});
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error);
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", {
|
||||
error_type
|
||||
});
|
||||
});
|
||||
|
||||
this.fetchers[url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing news fetcher for url: " + url);
|
||||
Log.log(`Use existing newsfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user