mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-21 12:55:22 +00:00
Compare commits
877 Commits
v2.17.1
...
a05eb23306
Author | SHA1 | Date | |
---|---|---|---|
|
a05eb23306 | ||
|
e115475a9d | ||
|
e4ec8c3589 | ||
|
d9e2e0272f | ||
|
3a2a52c864 | ||
|
855b1d7cbf | ||
|
106b505f2c | ||
|
b506bbb10b | ||
|
62b0f7f26e | ||
|
26809725e5 | ||
|
5e506ea856 | ||
|
1e11d28224 | ||
|
d2d4d7b37f | ||
|
2809ed1750 | ||
|
c7c0e67c1d | ||
|
9a3f4f098b | ||
|
ee874836fe | ||
|
6501aabd2d | ||
|
2194ffd929 | ||
|
faf15ad211 | ||
|
052ec1ca26 | ||
|
302b24c647 | ||
|
975ee9c97d | ||
|
c8625ff506 | ||
|
e26aed927d | ||
|
85b4ece767 | ||
|
4e3821c2ff | ||
|
d07912d4b2 | ||
|
d179051329 | ||
|
2f9f4b6253 | ||
|
8e0b8468d3 | ||
|
554bb0ed5c | ||
|
965e935881 | ||
|
2422e847b1 | ||
|
ed419ce5b3 | ||
|
53bff7243d | ||
|
2831ae985c | ||
|
7b4d363b07 | ||
|
a5b85c4ab6 | ||
|
b9d63d7252 | ||
|
ff6682982f | ||
|
e1a53ef2d5 | ||
|
86934c8375 | ||
|
7938c3a175 | ||
|
39a614e0de | ||
|
a2c1daa667 | ||
|
01fd41c191 | ||
|
f80b1f1321 | ||
|
e546fedeb1 | ||
|
2e57d785ac | ||
|
6d909d24e9 | ||
|
2ddb7859f6 | ||
|
791f77105a | ||
|
46d64abb4b | ||
|
68ec696c25 | ||
|
0cfe2730ea | ||
|
51d11bf26c | ||
|
0afc1ed58d | ||
|
2adf341fef | ||
|
1fcc028e49 | ||
|
66b8656595 | ||
|
4a398f03eb | ||
|
28bcee7de6 | ||
|
62c22d785c | ||
|
aa20eadca3 | ||
|
e00a666795 | ||
|
f34c8f2993 | ||
|
d6f2e7165f | ||
|
af77b7b628 | ||
|
53ac31dcf3 | ||
|
9c9a5359dd | ||
|
77fe01175c | ||
|
6e40c446f4 | ||
|
2400e2045f | ||
|
99dda821d3 | ||
|
a7f814d76b | ||
|
553d2d4a21 | ||
|
b1bc554729 | ||
|
5e337f8b5f | ||
|
8fdd865cb1 | ||
|
0f6efac8e6 | ||
|
75dbe67167 | ||
|
6f50a7b3bd | ||
|
c24de64d77 | ||
|
9be625c72b | ||
|
c92fbb8a7e | ||
|
143dfd6b67 | ||
|
d41ce81469 | ||
|
93a0c24c22 | ||
|
9d0501f240 | ||
|
6a09bc4ec4 | ||
|
2fb51436a5 | ||
|
0b3a04c520 | ||
|
c485ff670d | ||
|
b910c60eb2 | ||
|
24d9b70c4c | ||
|
786eacf41a | ||
|
5b3b40da66 | ||
|
5232f46d44 | ||
|
39ab651319 | ||
|
76fac78909 | ||
|
5b7b76c877 | ||
|
19bd76ab93 | ||
|
291ae8546c | ||
|
63178eba72 | ||
|
07768c3a88 | ||
|
8d61336e8b | ||
|
28341d4a54 | ||
|
f417bc0204 | ||
|
3627bebc3a | ||
|
bd1324cc42 | ||
|
15baffdede | ||
|
bd620e0061 | ||
|
f1522da153 | ||
|
56cb536df1 | ||
|
4259d7c075 | ||
|
cd6f10c843 | ||
|
b250cfa0ee | ||
|
7e6349c093 | ||
|
6ce3622c61 | ||
|
0aae771799 | ||
|
9114aefecc | ||
|
399e2ae1da | ||
|
c96326b760 | ||
|
cfa5c0d127 | ||
|
6946b49977 | ||
|
aa7e856170 | ||
|
b54fc08da7 | ||
|
0f024cff4e | ||
|
fff31068ab | ||
|
3d1e8ab849 | ||
|
1b80e87563 | ||
|
7489e51a67 | ||
|
0130dc45ab | ||
|
c7dcf542cf | ||
|
961bae637c | ||
|
f51fbe39c4 | ||
|
f91340ceca | ||
|
0eafa19096 | ||
|
ee98a0c7e5 | ||
|
4c7c859ae6 | ||
|
d1be92a426 | ||
|
15a934641d | ||
|
94c3c699e8 | ||
|
d84d612df5 | ||
|
719eca49fe | ||
|
d9eefff066 | ||
|
731512c2e5 | ||
|
ebaeed935f | ||
|
2e6e86887b | ||
|
d3187689f0 | ||
|
5ffdf9af09 | ||
|
08116b8e64 | ||
|
fa6a7521b4 | ||
|
06a8b517aa | ||
|
1823f5a130 | ||
|
8f5aa50d79 | ||
|
65d7e2d067 | ||
|
06f6fbf49b | ||
|
c6e05c9fec | ||
|
866419eb95 | ||
|
659e0c74cb | ||
|
d9f9f41e98 | ||
|
ea3a323581 | ||
|
0faefd109a | ||
|
81351fb4cc | ||
|
3380314c11 | ||
|
bca5d9c845 | ||
|
7915de3149 | ||
|
2b97e0d26e | ||
|
56736786fd | ||
|
cc1d4ab240 | ||
|
976c8ae00a | ||
|
780e4e2e06 | ||
|
76d8f98969 | ||
|
4182c2129f | ||
|
d22d0e1f87 | ||
|
d9665b35df | ||
|
974a1da9f0 | ||
|
4d14f4a2c1 | ||
|
3b22622054 | ||
|
160d95ac34 | ||
|
f7369a7e85 | ||
|
7389a33f80 | ||
|
53fc814ff8 | ||
|
795e5c76c1 | ||
|
92ac3895a7 | ||
|
c89c3edf97 | ||
|
74c6bb30b0 | ||
|
cfc0bc617f | ||
|
4aafa32875 | ||
|
f28b4bd709 | ||
|
aefb3a0b6d | ||
|
3d9d72e64e | ||
|
8d20832bc5 | ||
|
e95c144c3e | ||
|
4c748a4d32 | ||
|
9cbd30f296 | ||
|
bc27c46723 | ||
|
63324454a9 | ||
|
4bd66cb121 | ||
|
cd0bc5b160 | ||
|
d1c17e7fc0 | ||
|
3b0035760d | ||
|
1fa17883bc | ||
|
8aaad8e7ec | ||
|
1981601f0a | ||
|
5ea8a3469a | ||
|
2a883c393c | ||
|
53420f5be9 | ||
|
b262bf6144 | ||
|
72ef8235b1 | ||
|
e004b33fab | ||
|
d9926fad79 | ||
|
fd44445ec3 | ||
|
be63e365bd | ||
|
57549fa19c | ||
|
52cfbacd4d | ||
|
6de578edb3 | ||
|
d970214a0e | ||
|
c5f90501ef | ||
|
16af809559 | ||
|
1a745cfb92 | ||
|
90ff3402cb | ||
|
e5678f0291 | ||
|
c7d94a069e | ||
|
2f2d84bb5c | ||
|
313531d623 | ||
|
73140cdf37 | ||
|
08f8a5107a | ||
|
88a96fb529 | ||
|
db65cd60eb | ||
|
5fb5ef6cc7 | ||
|
57de389e01 | ||
|
431bf22adb | ||
|
3bf848075d | ||
|
fb5fab8145 | ||
|
7f0b8e4054 | ||
|
27f3c86c41 | ||
|
b0161fe011 | ||
|
f88b92fb1f | ||
|
339aaf4c01 | ||
|
c75b7d4a70 | ||
|
c96ced9137 | ||
|
995b61b689 | ||
|
c09338ab80 | ||
|
b005a8f30e | ||
|
35e4dfb3fe | ||
|
6dbacbb773 | ||
|
098757f248 | ||
|
58bc14e8c0 | ||
|
f890f14df7 | ||
|
dadc7ba0a2 | ||
|
b47600e0d8 | ||
|
4bbd35fa6a | ||
|
407072d12d | ||
|
6097547c10 | ||
|
5f7b56e645 | ||
|
bcab8ebd26 | ||
|
ae1f9d0468 | ||
|
367d02f1b6 | ||
|
5e346e7c0a | ||
|
118e21238c | ||
|
8c0e7db494 | ||
|
d20d9a7ef8 | ||
|
b8e0e2a791 | ||
|
a927eb20d9 | ||
|
aad3eefc62 | ||
|
ee1960ced0 | ||
|
0b70274a1a | ||
|
4e7b68a69d | ||
|
786ea86e0e | ||
|
d397568062 | ||
|
a7af76b619 | ||
|
319a921f75 | ||
|
d5406f4900 | ||
|
55cd03576f | ||
|
9d97724401 | ||
|
74854387cd | ||
|
e77f10b86d | ||
|
6ffdc7b55b | ||
|
7098f1e41f | ||
|
679a413788 | ||
|
247115d2e4 | ||
|
70ddd80632 | ||
|
203e8647d4 | ||
|
3fe5ad4b3d | ||
|
b300191609 | ||
|
296df06c21 | ||
|
fe882bf92a | ||
|
2a6e2aacdc | ||
|
3a01acd389 | ||
|
a8d06ae74e | ||
|
04f0df269a | ||
|
f80889d953 | ||
|
6815dfa02b | ||
|
bbc27f5ae2 | ||
|
f46b226940 | ||
|
764ca3ac5c | ||
|
0e2da630d5 | ||
|
a0b444d6c4 | ||
|
5d2ddbd3dd | ||
|
b067711ede | ||
|
66b29ec26e | ||
|
343e7de7bd | ||
|
6ea94e4512 | ||
|
290b350856 | ||
|
9566d6c9a0 | ||
|
6b204cda25 | ||
|
e530c783f8 | ||
|
a3c2e7b816 | ||
|
ad665a7a33 | ||
|
95ec3096e0 | ||
|
a67a0b677c | ||
|
8b1c279c07 | ||
|
4eccce3f77 | ||
|
af0fe37f70 | ||
|
e5adbea49c | ||
|
7127979c6f | ||
|
fa7c7fc8cf | ||
|
91fd931a58 | ||
|
7a1591b2d6 | ||
|
f2957f90df | ||
|
ffdf321e23 | ||
|
79e99e18ea | ||
|
a92b3d3f71 | ||
|
5cbdd28db3 | ||
|
9d49196e69 | ||
|
ef20fe2d11 | ||
|
c0a5f35a00 | ||
|
2ad463b6c7 | ||
|
200db181d5 | ||
|
7ba96aeb98 | ||
|
7c64d8fce6 | ||
|
7dcea98e99 | ||
|
59e9d765e2 | ||
|
156db32c76 | ||
|
58cdfa3cb1 | ||
|
49c72d8dfc | ||
|
1bd146f52e | ||
|
948910d304 | ||
|
0b97639341 | ||
|
f802c85a38 | ||
|
62eb23ba6a | ||
|
4b0e0aa48f | ||
|
e9f1bd9d7a | ||
|
e87f50e64a | ||
|
46bca1bc6d | ||
|
2b6720e6e5 | ||
|
ea818bf899 | ||
|
0e00e64493 | ||
|
3c35d346ee | ||
|
675e4d4f67 | ||
|
c1850f2577 | ||
|
e985e99036 | ||
|
b7371538bc | ||
|
a56b92990d | ||
|
c7405b76b3 | ||
|
eceec8285d | ||
|
0573d6e772 | ||
|
babd22b04f | ||
|
432d900ecd | ||
|
83315f1fed | ||
|
e09d60d1d1 | ||
|
d832d795df | ||
|
a41aa48dd1 | ||
|
b80485b52f | ||
|
7e58b38ddf | ||
|
979f4ec664 | ||
|
4e3369062e | ||
|
77f9c86774 | ||
|
dee3cd3da7 | ||
|
09f117c3d9 | ||
|
32192d1698 | ||
|
2c7beeaaaf | ||
|
0d3ad9812c | ||
|
b7eb21e48f | ||
|
9703226c73 | ||
|
cc11b77f24 | ||
|
abe5c08a52 | ||
|
c5a8b85f4e | ||
|
fa40a3e8e8 | ||
|
6223584392 | ||
|
b5a22bc09b | ||
|
4ef030af5f | ||
|
5f38c53260 | ||
|
d5395ee3f8 | ||
|
ab0876f07a | ||
|
d276a7ddb9 | ||
|
8f8945d418 | ||
|
6d779235cf | ||
|
beea754514 | ||
|
c6db22524a | ||
|
23ee155ded | ||
|
1b2785cc56 | ||
|
b5b61246e6 | ||
|
498b440174 | ||
|
fe0b915a5d | ||
|
2b792cdbb8 | ||
|
a23769156e | ||
|
6d86ffade4 | ||
|
390e5d6490 | ||
|
b08a4737af | ||
|
bf28e63709 | ||
|
fb22a76796 | ||
|
81244d961e | ||
|
65aa1b0ddc | ||
|
88c7e42368 | ||
|
e24dfa6b1a | ||
|
a65ee86501 | ||
|
4b478a5a5e | ||
|
f14e956166 | ||
|
1dc0a0d5b5 | ||
|
bf279d9a57 | ||
|
42d42ef452 | ||
|
ed90f0546f | ||
|
a8dc563a31 | ||
|
58b9ddcd9f | ||
|
7198ae5eae | ||
|
f6dcfb5ca3 | ||
|
157e74ce7c | ||
|
67e4dbaacd | ||
|
2e2962d492 | ||
|
cd4ba428da | ||
|
ee8695637b | ||
|
4244c05764 | ||
|
c714399b4d | ||
|
8d9f132666 | ||
|
d2327d3d6f | ||
|
29e3ec06cb | ||
|
877f8ad380 | ||
|
6e80e5a295 | ||
|
7bc91a742f | ||
|
2eaf9dfeeb | ||
|
fc303146a5 | ||
|
2908c15ea6 | ||
|
a975b44fbb | ||
|
4fc38bd5bb | ||
|
c99f660d98 | ||
|
0300ce05d5 | ||
|
cd739b6912 | ||
|
0ebedd0fb8 | ||
|
e9be668d1b | ||
|
76d9042e60 | ||
|
2fec314ff5 | ||
|
3124b0a9c5 | ||
|
a2624442cc | ||
|
eee289aee8 | ||
|
abbae90a8f | ||
|
bd0b3c00ad | ||
|
b9b7d2c95d | ||
|
0b01e9dbe0 | ||
|
4fecffc3df | ||
|
4d47c0837f | ||
|
3879949f58 | ||
|
f25abfd2f8 | ||
|
7058fc5fd8 | ||
|
00bc6eb28c | ||
|
f79d3f007d | ||
|
c191ff0032 | ||
|
dde88601a6 | ||
|
2d3940a4ff | ||
|
64ed5a54cb | ||
|
7bd944391e | ||
|
ad4dbd786a | ||
|
fc59ed20e3 | ||
|
835c893205 | ||
|
7bbf8c19db | ||
|
1eb2965b2b | ||
|
85a9f14178 | ||
|
a328ce537f | ||
|
21ae79b386 | ||
|
d5e855dd6d | ||
|
a86e27a12c | ||
|
f434be3d44 | ||
|
ce4906d13b | ||
|
8212d30c4c | ||
|
f04d578704 | ||
|
7694d6fa86 | ||
|
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 | ||
|
1735ca57d5 | ||
|
a49459b253 | ||
|
332e429a41 | ||
|
7be25c21ed | ||
|
1dfa5d383c | ||
|
c2d2c278e0 | ||
|
75a57829c2 |
@@ -1 +0,0 @@
|
||||
modules/default/calendar/vendor/*
|
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc", "jest"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"config": true,
|
||||
"Log": true,
|
||||
"MM": true,
|
||||
"Module": true,
|
||||
"moment": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "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
|
137
.github/CODE_OF_CONDUCT.md
vendored
Normal file
137
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement:
|
||||
Contact [Rejas](https://forum.magicmirror.builders/user/rejas),
|
||||
[Karsten](https://forum.magicmirror.builders/user/karsten13),
|
||||
[Sam](https://forum.magicmirror.builders/user/sdetweil) or
|
||||
[Kristjan](https://forum.magicmirror.builders/user/kristjanesperanto)
|
||||
via private message in the forum.
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
45
.github/CONTRIBUTING.md
vendored
45
.github/CONTRIBUTING.md
vendored
@@ -4,46 +4,35 @@ 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](https://prettier.io/) for automatic formatting a lot all our files. The configuration is in our `prettier.config.mjs` file.
|
||||
|
||||
To run prettier, use `node --run lint:prettier`.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
We use [ESLint](https://eslint.org) to lint our JavaScript files. The configuration is in our `eslint.config.mjs` file.
|
||||
|
||||
Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
To run ESLint, use `node --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. The configuration is in our `stylelint.config.mjs` file.
|
||||
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
To run StyleLint, use `node --run lint:css`.
|
||||
|
||||
### Submitting Issues
|
||||
### Markdown: Run markdownlint
|
||||
|
||||
Please only submit reproducible issues.
|
||||
We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) to lint our markdown files. The configuration is in our `.markdownlint.json` file.
|
||||
|
||||
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)
|
||||
To run markdownlint, use `node --run lint:markdown`.
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
## Testing
|
||||
|
||||
When submitting a new issue, please supply the following information:
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
|
||||
**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).
|
||||
To run all tests, use `node --run test`.
|
||||
|
||||
**Node Version**: Make sure it's version 12 or later (recommended is 14).
|
||||
|
||||
**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.
|
||||
|
||||
**Steps to Reproduce**: List the step by step process to reproduce the issue.
|
||||
|
||||
**Expected Results**: Describe what you expected to see.
|
||||
|
||||
**Actual Results**: Describe what you actually saw.
|
||||
|
||||
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
|
||||
|
||||
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.
|
||||
The specific test commands are defined in `package.json`.
|
||||
So you can also run the specific tests with other commands, e.g. `node --run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
50
.github/ISSUE_TEMPLATE.md
vendored
50
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,50 +0,0 @@
|
||||
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)
|
||||
|
||||
## I'm having troubles installing or configuring MagicMirror
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
|
||||
|
||||
## 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 corresponding repository:
|
||||
|
||||
- karsten13/magicmirror: [https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
|
||||
- (deprecated) bastilimbach/docker-magicmirror: [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
|
||||
|
||||
---
|
||||
|
||||
## I found a bug in MagicMirror
|
||||
|
||||
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
|
||||
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 12 or later (recommended is 14).
|
||||
|
||||
**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.
|
||||
|
||||
**Steps to Reproduce**: List the step by step process to reproduce the issue.
|
||||
|
||||
**Expected Results**: Describe what you expected to see.
|
||||
|
||||
**Actual Results**: Describe what you actually saw.
|
||||
|
||||
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
|
||||
|
||||
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.
|
154
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
154
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
name: 🐛 Report a problem
|
||||
description: Report an issue with MagicMirror² 🚨
|
||||
title: "[Bug] {{ brief description }}"
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug! Please fill in the following template to help us reproduce the issue.
|
||||
Please only submit reproducible issues. If you're not sure if it's a real bug or if it's just you, please open a topic on the forum.
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Please tell us about how your MagicMirror² is set up.
|
||||
|
||||
Optimal would be the systeminformation from the logs, which looks like this:
|
||||
```bash
|
||||
[2025-01-14 20:05:03.529] [INFO] System information:
|
||||
### SYSTEM: manufacturer: Raspberry Pi Foundation; model: Raspberry Pi 4 Model B Rev 1.5; virtual: false
|
||||
### OS: platform: linux; distro: Debian GNU/Linux; release: 12; arch: arm64; kernel: 6.1.21-v8+
|
||||
### VERSIONS: electron: 31.2.1; used node: 20.15.0; installed node: 22.4.1; npm: 10.8.1; pm2:
|
||||
### OTHER: timeZone: Europe/Berlin; ELECTRON_ENABLE_GPU: undefined
|
||||
```
|
||||
|
||||
If you can't provide this information, please provide the following:
|
||||
- MagicMirror² version: Can be found in the `package.json` file. Please use the latest version before reporting a bug.
|
||||
- Node version: Run `node -v` to find out. Make sure it's version 20 or later (recommended is 22).
|
||||
- npm version: Run `npm -v` to find out.
|
||||
- Platform: Are you using a Raspberry Pi (2/3/4/5), Windows, Mac, Linux, Docker, or something else?
|
||||
value: |
|
||||
MagicMirror² version:
|
||||
Node version:
|
||||
npm version:
|
||||
Platform:
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: start-option
|
||||
attributes:
|
||||
label: Which start option are you using?
|
||||
description: |
|
||||
Please keep in mind that some problems are specific to certain start options.
|
||||
options:
|
||||
- "node --run start"
|
||||
- "node --run start:wayland"
|
||||
- "node --run start:windows"
|
||||
- "node --run start:x11"
|
||||
- "node --run server"
|
||||
- "node clientonly --address ... --port ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: pm2
|
||||
attributes:
|
||||
label: Are you using PM2?
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
- "I don't know"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: Module
|
||||
description: |
|
||||
If the issue is related to a specific module, please provide the name of the module.
|
||||
Note: Please don't report issues with 3rd party modules here. Report them on the module's repository.
|
||||
options:
|
||||
- "alert"
|
||||
- "calendar"
|
||||
- "clock"
|
||||
- "compliments"
|
||||
- "helloworld"
|
||||
- "newsfeed"
|
||||
- "updatenotification"
|
||||
- "weather"
|
||||
- type: checkboxes
|
||||
id: module-disabled
|
||||
attributes:
|
||||
label: Have you tried disabling other modules?
|
||||
options:
|
||||
- label: "Yes"
|
||||
- label: "No"
|
||||
- type: checkboxes
|
||||
id: search
|
||||
attributes:
|
||||
label: Have you searched if someone else has already reported the issue on the forum or in the issues?
|
||||
options:
|
||||
- label: "Yes"
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What did you do?
|
||||
description: |
|
||||
Please include a *minimal* reproduction case. List the step by step process to reproduce the issue.
|
||||
You can use Markdown in this field.
|
||||
value: |
|
||||
<details>
|
||||
<summary>Configuration</summary>
|
||||
|
||||
```
|
||||
<!-- Paste your configuration here. Don't forget to remove any sensitive information! -->
|
||||
```
|
||||
</details>
|
||||
|
||||
```js
|
||||
<!-- Paste relevant code here -->
|
||||
```
|
||||
|
||||
Steps to reproduce the issue:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
description: |
|
||||
You can use Markdown in this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: lint-output
|
||||
attributes:
|
||||
label: What actually happened?
|
||||
description: |
|
||||
Please copy-paste relevant log output or error messages.
|
||||
You can use Markdown in this field.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: comments
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: |
|
||||
Is there anything else that's important for the team to know?
|
||||
Fill out all fields and provide as much information as possible.
|
||||
Adding screenshots might help us understand your problem better.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: "I am willing to submit a pull request for this change."
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: 🔀 Request a change
|
||||
description: Request a change that is not a bug fix, a feature request or a support request.
|
||||
title: "[Change Request] {{ brief description }}"
|
||||
labels:
|
||||
- enhancement
|
||||
- core
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for requesting a change! Please fill in the following template to help us understand your request.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What problem do you want to solve with this change?
|
||||
description: |
|
||||
Please explain your use case in as much detail as possible.
|
||||
placeholder: |
|
||||
Currently...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What do you think is the correct solution?
|
||||
description: |
|
||||
Please explain how you'd like to change MagicMirror² to address the problem.
|
||||
placeholder: |
|
||||
I'd like MagicMirror² to...
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this change.
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: Is there anything else that's important for the team to know?
|
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📚 Documentation
|
||||
url: https://github.com/MagicMirrorOrg/MagicMirror-Documentation/issues
|
||||
about: This issue tracker is not for documentation issues. Please file documentation issues on the docs repo.
|
||||
- name: 🤔 Support Question
|
||||
url: https://forum.magicmirror.builders/
|
||||
about: Problems installing or configuring your MagicMirror? Please post your question on the MagicMirror² Forum.
|
||||
- name: 💬 Exchange of ideas
|
||||
url: https://discord.gg/AmGBBwPph5
|
||||
about: This issue tracker is not for general discussion. Please use the Discord channel.
|
||||
- name: 📦 Issues with a 3rd-party module
|
||||
url: https://kristjanesperanto.github.io/MagicMirror-3rd-Party-Modules/
|
||||
about: This issue tracker is not for 3rd-party module issues. Please file 3rd-party module issues on the module's repo.
|
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Suggest a new feature for MagicMirror² 💡
|
||||
title: "[Feature Request] {{ brief description }}"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please ensure you have completed all of the following.
|
||||
options:
|
||||
- label: I am running the latest version of MagicMirror², and know that this feature is not available now.
|
||||
required: true
|
||||
- label: I know my issue is not related to a third-party module.
|
||||
required: true
|
||||
- label: I have searched for [existing issues](https://github.com/MagicMirrorOrg/MagicMirror/issues) that already include this feature request, without success.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the Feature Request
|
||||
description: A clear and concise description of what the feature does.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: use-case
|
||||
attributes:
|
||||
label: Describe the Use Case
|
||||
description: A clear and concise use case for what problem this feature would solve.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: Describe Preferred Solution
|
||||
description: A clear and concise description of how you want this feature to be added to MagicMirror².
|
||||
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
description: A clear and concise description of any alternative solutions or features you have considered.
|
||||
|
||||
- type: textarea
|
||||
id: related-code
|
||||
attributes:
|
||||
label: Related Code
|
||||
description: If you are able to illustrate the feature request with an example, please provide a sample here.
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: List any other information that is relevant to your issue. Related issues, suggestions on how to implement, Stack Overflow links, forum links, etc.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this change.
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +1,8 @@
|
||||
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.
|
||||
>
|
||||
> 2. Include these infos in the description:
|
||||
>
|
||||
> - Does the pull request solve a **related** issue?
|
||||
@@ -11,9 +10,8 @@ Hello and thank you for wanting to contribute to the MagicMirror project
|
||||
> - 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 `node --run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
> 4. Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
|
6
.github/codecov.yml
vendored
6
.github/codecov.yml
vendored
@@ -1,6 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
20
.github/dependabot.yaml
vendored
Normal file
20
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
- "javascript"
|
19
.github/stale.yml
vendored
19
.github/stale.yml
vendored
@@ -1,19 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- under investigation
|
||||
- pr welcome
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
69
.github/workflows/automated-tests.yaml
vendored
Normal file
69
.github/workflows/automated-tests.yaml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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:
|
||||
code-style-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: "Run linter tests"
|
||||
run: |
|
||||
node --run test:prettier
|
||||
node --run test:js
|
||||
node --run test:css
|
||||
node --run test:markdown
|
||||
test:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.14.0, 22.x, 24.x]
|
||||
steps:
|
||||
- name: Install electron dependencies and labwc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libnss3 libasound2t64 labwc
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
- name: "Install MagicMirror²"
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: "Prepare environment for tests"
|
||||
run: |
|
||||
# Fix chrome-sandbox permissions:
|
||||
sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
|
||||
# Start labwc
|
||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman labwc &
|
||||
touch css/custom.css
|
||||
- name: "Run tests"
|
||||
run: |
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
node --run test
|
37
.github/workflows/automated-tests.yml
vendored
37
.github/workflows/automated-tests.yml
vendored
@@ -1,37 +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
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies and run tests
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
touch css/custom.css
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test:unit
|
||||
npm run test:e2e
|
||||
npm run test:electron
|
29
.github/workflows/codecov-test-suites.yml
vendored
29
.github/workflows/codecov-test-suites.yml
vendored
@@ -1,29 +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
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install dependencies and run coverage
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- name: Upload coverage results to codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
18
.github/workflows/dep-review.yaml
vendored
Normal file
18
.github/workflows/dep-review.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@v4
|
32
.github/workflows/electron-rebuild.yaml
vendored
Normal file
32
.github/workflows/electron-rebuild.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: "Electron Rebuild Testing"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
rebuild:
|
||||
name: Run electron-rebuild
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.14.0, 22.x, 24.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
- name: Install MagicMirror
|
||||
run: node --run install-mm
|
||||
- name: Install @electron/rebuild
|
||||
run: npm install @electron/rebuild
|
||||
- name: Install node-libgpiod deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gpiod libgpiod2 libgpiod-dev
|
||||
- name: Install test library (node-libgpiod) to be rebuilded
|
||||
run: npm install node-libgpiod
|
||||
- name: Run electron-rebuild
|
||||
run: npx electron-rebuild
|
||||
continue-on-error: false
|
20
.github/workflows/enforce-changelog.yml
vendored
20
.github/workflows/enforce-changelog.yml
vendored
@@ -1,20 +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
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Enforce changelog️
|
||||
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_target:
|
||||
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.event.pull_request.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.event.pull_request.base.ref }}
|
31
.github/workflows/spellcheck.yaml
vendored
Normal file
31
.github/workflows/spellcheck.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# This workflow will run a spellcheck on the codebase.
|
||||
# It runs a few days before each release. At 00:00 on day-of-month 27 in March, June, September, and December.
|
||||
|
||||
name: Run Spellcheck
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 27 3,6,9,12 *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: Run Spellcheck
|
||||
run: node --run test:spelling
|
22
.github/workflows/stale.yaml
vendored
Normal file
22
.github/workflows/stale.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: "Close stale issues and PRs"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # needed for manually running this workflow
|
||||
schedule:
|
||||
- cron: "30 1 * * 6" # every Saturday at 1:30
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions."
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 7
|
||||
operations-per-run: 100
|
||||
stale-issue-label: "wontfix"
|
||||
exempt-issue-labels: "pinned,security,under investigation,pr welcome,ready (coming with next release)"
|
18
.gitignore
vendored
18
.gitignore
vendored
@@ -10,13 +10,10 @@ coverage
|
||||
.lock-wscript
|
||||
build/Release
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
!/tests/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output/
|
||||
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
@@ -64,8 +61,12 @@ Temporary Items
|
||||
!/modules/default/**
|
||||
!/modules/README.md**
|
||||
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
# Ignore changes to the custom css files but keep the sample and main.
|
||||
/css/*
|
||||
!/css/custom.css.sample
|
||||
!/css/main.css
|
||||
!/css/roboto.css
|
||||
!/css/font-awesome.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
@@ -80,3 +81,10 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
# Ignore positions file (#3518)
|
||||
js/positions.js
|
||||
|
||||
# Ignore lock files other than package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
||||
if command -v npx &> /dev/null; then
|
||||
npx lint-staged
|
||||
fi
|
||||
|
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"line_length": false,
|
||||
"no-duplicate-heading": false,
|
||||
"no-inline-html": false,
|
||||
"no-trailing-punctuation": false
|
||||
}
|
@@ -1,4 +1,8 @@
|
||||
*.js
|
||||
*.mjs
|
||||
.husky/pre-commit
|
||||
.prettierignore
|
||||
/config
|
||||
/coverage
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
**.ics
|
||||
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none"
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
}
|
||||
}
|
987
CHANGELOG.md
987
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
67
Collaboration.md
Normal file
67
Collaboration.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Collaboration
|
||||
|
||||
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)
|
||||
- merge to `master` only for releases or other urgent issues (update notification is only triggered by tags)
|
||||
- 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
|
||||
|
||||
- [ ] @rejas
|
||||
- [ ] @sdetweil
|
||||
- [ ] @khassel
|
||||
|
||||
### Pre-Deployment steps
|
||||
|
||||
- [ ] update dependencies (a few days before)
|
||||
|
||||
### Deployment steps
|
||||
|
||||
- [ ] pull latest `develop` branch
|
||||
- [ ] create `prep-release` branch from `develop`
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
|
||||
- [ ] test `prep-release` branch
|
||||
- [ ] update `CHANGELOG.md`
|
||||
- [ ] add all contributor names: `...`
|
||||
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher
|
||||
- [ ] check release link at the bottom of the file
|
||||
- [ ] commit and push all changes
|
||||
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
|
||||
- [ ] after successful test run via github actions: merge pull request to `develop`
|
||||
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
|
||||
- [ ] add label `mastermerge`
|
||||
- [ ] title of the PR is `Release 2.xx.0`
|
||||
- [ ] description of the PR is the section of the `CHANGELOG.md`
|
||||
- [ ] after PR tests run without issues, merge PR
|
||||
- [ ] create new release with
|
||||
- [ ] corresponding version tag `v2.xx.0`
|
||||
- [ ] a release name: `...`
|
||||
- [ ] description of the release is the section of the `CHANGELOG.md`
|
||||
|
||||
### Draft new development release
|
||||
|
||||
- [ ] checkout `develop` branch
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop`
|
||||
- [ ] draft new section in `CHANGELOG.md`
|
||||
- [ ] create new release link at the bottom of the file
|
||||
- [ ] commit and push `develop` branch
|
||||
- [ ] if new release will be in January, update the year in LICENSE.md
|
||||
|
||||
### After release
|
||||
|
||||
- [ ] publish release notes with link to github release on forum in new locked topic
|
||||
- [ ] close all issues with label `ready (coming with next release)`
|
||||
- [ ] release new documentation by merging `develop` on `master` in documentation repository
|
||||
- [ ] publish new version on [npm](https://www.npmjs.com/package/magicmirror)
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
Copyright © 2016-2025 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
26
README.md
26
README.md
@@ -1,15 +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://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/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/check-runs/magicmirrororg/magicmirror/master" alt="Build Status">
|
||||
<a href="https://github.com/MagicMirrorOrg/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/magicmirrororg/magicmirror?style=social" alt="GitHub Stars">
|
||||
</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).
|
||||
**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/MagicMirrorOrg/MagicMirror/graphs/contributors).
|
||||
|
||||
MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary!
|
||||
|
||||
@@ -22,7 +24,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
|
||||
- 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,11 +37,11 @@ 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!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
MagicMirror² is Open Source and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
@@ -47,5 +49,5 @@ If we receive enough donations we might even be able to free up some working hou
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
<p style="text-align: center">
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
</p>
|
||||
|
@@ -7,16 +7,16 @@
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
*/
|
||||
function getServerAddress() {
|
||||
function getServerAddress () {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
function getCommandLineParameter (key, defaultValue = undefined) {
|
||||
const index = process.argv.indexOf(`--${key}`);
|
||||
const value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
return value !== undefined ? String(value) : defaultValue;
|
||||
@@ -28,20 +28,19 @@
|
||||
});
|
||||
|
||||
// determine if "--use-tls"-flag was provided
|
||||
config["tls"] = process.argv.indexOf("--use-tls") > 0;
|
||||
config.tls = process.argv.indexOf("--use-tls") > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
function getServerConfig(url) {
|
||||
function getServerConfig (url) {
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const lib = url.startsWith("https") ? require("node:https") : require("node:http");
|
||||
const request = lib.get(url, (response) => {
|
||||
let configData = "";
|
||||
|
||||
@@ -63,11 +62,10 @@
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function fail(message, code = 1) {
|
||||
function fail (message, code = 1) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
console.log(message);
|
||||
} else {
|
||||
@@ -85,8 +83,20 @@
|
||||
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) {
|
||||
// check environment for DISPLAY or WAYLAND_DISPLAY
|
||||
const elecParams = ["js/electron.js"];
|
||||
if (process.env.WAYLAND_DISPLAY) {
|
||||
console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`);
|
||||
elecParams.push("--enable-features=UseOzonePlatform");
|
||||
elecParams.push("--ozone-platform=wayland");
|
||||
} else if (process.env.DISPLAY) {
|
||||
console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`);
|
||||
} else {
|
||||
fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided.");
|
||||
}
|
||||
// Pass along the server config via an environment variable
|
||||
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;
|
||||
@@ -95,7 +105,7 @@
|
||||
|
||||
// Spawn electron application
|
||||
const electron = require("electron");
|
||||
const child = require("child_process").spawn(electron, ["js/electron.js"], options);
|
||||
const child = require("node:child_process").spawn(electron, elecParams, options);
|
||||
|
||||
// Pipe all child process output to current stdout
|
||||
child.stdout.on("data", function (buf) {
|
||||
@@ -123,4 +133,4 @@
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
})();
|
||||
}());
|
||||
|
@@ -1,41 +1,41 @@
|
||||
/* Magic Mirror Config Sample
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
/* Config Sample
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
|
||||
* 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
|
||||
*/
|
||||
let config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
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
|
||||
// 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 :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
|
||||
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
|
||||
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 :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
|
||||
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
|
||||
|
||||
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
|
||||
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
|
||||
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
locale: "en-US", // this variable is provided as a consistent location
|
||||
// it is currently only used by 3rd party modules. no MagicMirror code uses this value
|
||||
// as we have no usage, we have no constraints on what this field holds
|
||||
// see https://en.wikipedia.org/wiki/Locale_(computer_software) for the possibilities
|
||||
|
||||
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: [
|
||||
{
|
||||
@@ -56,8 +56,9 @@ let 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -70,11 +71,10 @@ let config = {
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -82,11 +82,10 @@ let config = {
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "forecast",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -109,4 +108,4 @@ let config = {
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
||||
if (typeof module !== "undefined") { module.exports = config; }
|
||||
|
253
cspell.config.json
Normal file
253
cspell.config.json
Normal file
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"aarch",
|
||||
"Alvinger",
|
||||
"Ampio",
|
||||
"andrezibaia",
|
||||
"angeldeejay",
|
||||
"apiontek",
|
||||
"armv",
|
||||
"ashishtank",
|
||||
"autoplay",
|
||||
"beada",
|
||||
"Binney",
|
||||
"bluemanos",
|
||||
"bnitkin",
|
||||
"bokmål",
|
||||
"Brasileiro",
|
||||
"Brento",
|
||||
"browserwindow",
|
||||
"bryanzzhu",
|
||||
"btoconnor",
|
||||
"bugsounet",
|
||||
"buxxi",
|
||||
"byday",
|
||||
"calendarfetcherutils",
|
||||
"calendarutils",
|
||||
"chamakura",
|
||||
"cjbrunner",
|
||||
"clientonly",
|
||||
"clockfaces",
|
||||
"cmdline",
|
||||
"codac",
|
||||
"Crazylegstoo",
|
||||
"crazyscot",
|
||||
"Creepin",
|
||||
"currentweather",
|
||||
"CUSTOMCSS",
|
||||
"customregions",
|
||||
"cxmj",
|
||||
"Cymraeg",
|
||||
"dariom",
|
||||
"darksky",
|
||||
"dateheader",
|
||||
"dateheaders",
|
||||
"davide",
|
||||
"DAYAFTERTOMORROW",
|
||||
"DAYBEFOREYESTERDAY",
|
||||
"defaultmodules",
|
||||
"dgoth",
|
||||
"dkallen",
|
||||
"drivelist",
|
||||
"DTEND",
|
||||
"DTSTAMP",
|
||||
"DTSTART",
|
||||
"Duffman",
|
||||
"earlman",
|
||||
"easyas",
|
||||
"eddiehung",
|
||||
"Edgardos",
|
||||
"Ekristoffe",
|
||||
"elec",
|
||||
"eltociear",
|
||||
"envcanada",
|
||||
"envsub",
|
||||
"envsubst",
|
||||
"eouia",
|
||||
"exdate",
|
||||
"expectedheaders",
|
||||
"ezeholz",
|
||||
"Faizan",
|
||||
"feedme",
|
||||
"feelslike",
|
||||
"Fenner",
|
||||
"fewieden",
|
||||
"fixuppm",
|
||||
"flopp",
|
||||
"fontawesome",
|
||||
"fontface",
|
||||
"forecastweather",
|
||||
"fortawesome",
|
||||
"frameguard",
|
||||
"Frysk",
|
||||
"fulldate",
|
||||
"fullday",
|
||||
"fullscreen",
|
||||
"geraki",
|
||||
"Gevoelstemperatuur",
|
||||
"GHSA",
|
||||
"ghsas",
|
||||
"grenagit",
|
||||
"Heiko",
|
||||
"Hirschberger",
|
||||
"hourlyweather",
|
||||
"Hwind",
|
||||
"ical",
|
||||
"illimarkangur",
|
||||
"Ingan",
|
||||
"ipfilter",
|
||||
"ismarslomic",
|
||||
"jakemulley",
|
||||
"jakobsarwary",
|
||||
"jalibu",
|
||||
"jargordon",
|
||||
"jetson",
|
||||
"jkriegshauser",
|
||||
"jsdocs",
|
||||
"jsonlint",
|
||||
"jupadin",
|
||||
"kaennchenstruggle",
|
||||
"Kalenderwoche",
|
||||
"kenzal",
|
||||
"Keyport",
|
||||
"khassel",
|
||||
"Kingdon",
|
||||
"kioskmode",
|
||||
"klaernie",
|
||||
"kleinmantara",
|
||||
"Kmph",
|
||||
"Knapoc",
|
||||
"Koepke",
|
||||
"kolbyjack",
|
||||
"krekos",
|
||||
"Kristjan",
|
||||
"krukle",
|
||||
"labwc",
|
||||
"Landis",
|
||||
"larryare",
|
||||
"letsencrypt",
|
||||
"libgpiod",
|
||||
"Lightspeed",
|
||||
"locationforecast",
|
||||
"lockstring",
|
||||
"lstrip",
|
||||
"Luciella",
|
||||
"luxon",
|
||||
"lxsession",
|
||||
"magicmirror",
|
||||
"martingron",
|
||||
"marvai",
|
||||
"mastermerge",
|
||||
"matchtype",
|
||||
"maxentries",
|
||||
"Meteo",
|
||||
"michaelteeuw",
|
||||
"michmich",
|
||||
"Midori",
|
||||
"mirontoli",
|
||||
"MISSINGLANG",
|
||||
"mixasgr",
|
||||
"MMPM",
|
||||
"modernizr",
|
||||
"modulename",
|
||||
"multiday",
|
||||
"Mystara",
|
||||
"Ñandú",
|
||||
"nathannaveen",
|
||||
"naveensrinivasan",
|
||||
"ndom",
|
||||
"Nerfzooka",
|
||||
"NEWSFEED",
|
||||
"newsitems",
|
||||
"nfogal",
|
||||
"njwilliams",
|
||||
"nonrepeating",
|
||||
"Norsk",
|
||||
"nunjuck",
|
||||
"odroid",
|
||||
"oemel",
|
||||
"onecall",
|
||||
"onevent",
|
||||
"openmeteo",
|
||||
"openweathermap",
|
||||
"oraclesean",
|
||||
"oscarb",
|
||||
"philnagel",
|
||||
"Português",
|
||||
"PRECIP",
|
||||
"Problema",
|
||||
"psieg",
|
||||
"radokristof",
|
||||
"rajniszp",
|
||||
"rebuilded",
|
||||
"Reis",
|
||||
"rejas",
|
||||
"Resig",
|
||||
"roboto",
|
||||
"rohitdharavath",
|
||||
"Rosso",
|
||||
"rrule",
|
||||
"savvadam",
|
||||
"sdetweil",
|
||||
"sendheaders",
|
||||
"serveronly",
|
||||
"sexualized",
|
||||
"skpanagiotis",
|
||||
"SMHI",
|
||||
"Snille",
|
||||
"socketclient",
|
||||
"socketio",
|
||||
"spectron",
|
||||
"Starinvest",
|
||||
"sthuber",
|
||||
"Stieber",
|
||||
"stylelintrc",
|
||||
"subclassing",
|
||||
"sunaction",
|
||||
"suncalc",
|
||||
"suntimes",
|
||||
"symboltest",
|
||||
"systeminformation",
|
||||
"tada",
|
||||
"taglist",
|
||||
"Teeuw",
|
||||
"TESTMODE",
|
||||
"thomasrockhu",
|
||||
"tomzt",
|
||||
"ukmetoffice",
|
||||
"ukmetofficedatahub",
|
||||
"unitless",
|
||||
"unparseable",
|
||||
"updatenotification",
|
||||
"Vaice",
|
||||
"veeck",
|
||||
"VEVENT",
|
||||
"vgtu",
|
||||
"Voelt",
|
||||
"vppencilsharpener",
|
||||
"Wallys",
|
||||
"Weatherbit",
|
||||
"WEATHERDATA",
|
||||
"Weatherflow",
|
||||
"weatherforecast",
|
||||
"weathergov",
|
||||
"weathericons",
|
||||
"weatherobject",
|
||||
"weatherutils",
|
||||
"windspeed",
|
||||
"Woolridge",
|
||||
"worktree",
|
||||
"xlarge",
|
||||
"xrandr",
|
||||
"xsmall",
|
||||
"xsorifc",
|
||||
"xwindows",
|
||||
"xxxe",
|
||||
"Ybbet",
|
||||
"yearmatchgroup"
|
||||
],
|
||||
"ignorePaths": ["node_modules/**", "modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "css/roboto.css"],
|
||||
"dictionaries": ["node"]
|
||||
}
|
@@ -1,10 +1,8 @@
|
||||
/* Magic Mirror Custom CSS Sample
|
||||
/* 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: */
|
||||
@@ -18,7 +16,7 @@
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
@@ -26,6 +24,6 @@
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
52
css/main.css
52
css/main.css
@@ -3,18 +3,18 @@
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: #000;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -60,27 +60,27 @@ body {
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-xsmall);
|
||||
line-height: 1.275;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 1.5rem;
|
||||
font-size: var(--font-size-medium);
|
||||
line-height: 1.225;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 3.25rem;
|
||||
font-size: var(--font-size-large);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 3.75rem;
|
||||
font-size: var(--font-size-xlarge);
|
||||
line-height: 1;
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ body {
|
||||
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--color-text-dimmed);
|
||||
@@ -138,6 +138,14 @@ sup {
|
||||
margin-bottom: var(--gap-modules);
|
||||
}
|
||||
|
||||
.module.hidden {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.module:not(.hidden) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.region.bottom .module {
|
||||
margin-top: var(--gap-modules);
|
||||
margin-bottom: 0;
|
||||
@@ -163,17 +171,10 @@ sup {
|
||||
|
||||
.region.fullscreen {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--gap-body-top));
|
||||
left: calc(-1 * var(--gap-body-left));
|
||||
right: calc(-1 * var(--gap-body-right));
|
||||
bottom: calc(-1 * var(--gap-body-bottom));
|
||||
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;
|
||||
@@ -238,3 +239,16 @@ sup {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container Definitions.
|
||||
*/
|
||||
|
||||
.region .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.region .container.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
671
css/roboto.css
Normal file
671
css/roboto.css
Normal file
@@ -0,0 +1,671 @@
|
||||
/* roboto-cyrillic-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100;
|
||||
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;
|
||||
font-style: normal;
|
||||
font-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: 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: 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: 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: 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: 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: swap;
|
||||
font-weight: 300;
|
||||
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-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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: swap;
|
||||
font-weight: 500;
|
||||
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: 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: 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: 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: 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: 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: 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: swap;
|
||||
font-weight: 700;
|
||||
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-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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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;
|
||||
}
|
132
eslint.config.mjs
Normal file
132
eslint.config.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
import {defineConfig, globalIgnores} from "eslint/config";
|
||||
import globals from "globals";
|
||||
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import js from "@eslint/js";
|
||||
import jsdoc from "eslint-plugin-jsdoc";
|
||||
import packageJson from "eslint-plugin-package-json";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
Log: "readonly",
|
||||
MM: "readonly",
|
||||
Module: "readonly",
|
||||
config: "readonly",
|
||||
moment: "readonly"
|
||||
}
|
||||
},
|
||||
plugins: {js, jsdoc, stylistic},
|
||||
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/max-statements-per-line": ["error", {max: 2}],
|
||||
"@stylistic/multiline-comment-style": "off",
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"dot-notation": "error",
|
||||
eqeqeq: "error",
|
||||
"id-length": "off",
|
||||
"import-x/extensions": "error",
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/order": "error",
|
||||
"init-declarations": "off",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"max-lines-per-function": ["warn", 400],
|
||||
"max-statements": "off",
|
||||
"no-global-assign": "off",
|
||||
"no-inline-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-param-reassign": "error",
|
||||
"no-plusplus": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"no-warning-comments": "off",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"one-var": "off",
|
||||
"prefer-template": "error",
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
ignores: [
|
||||
"clientonly/index.js",
|
||||
"modules/default/calendar/debug.js",
|
||||
"js/logger.js",
|
||||
"tests/**/*.js"
|
||||
],
|
||||
rules: {"no-console": "error"}
|
||||
},
|
||||
{
|
||||
files: ["**/package.json"],
|
||||
plugins: {packageJson},
|
||||
extends: ["packageJson/recommended"]
|
||||
},
|
||||
{
|
||||
files: ["**/*.mjs"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
},
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: {js, stylistic},
|
||||
extends: [importX.recommended, "js/all", "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": "off",
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/padded-blocks": ["error", "never"],
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}],
|
||||
"max-lines-per-function": ["error", 100],
|
||||
"no-magic-numbers": "off",
|
||||
"one-var": ["error", "never"],
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/configs/modules/weather/*.js"],
|
||||
rules: {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
}
|
||||
]);
|
26
fonts/package-lock.json
generated
26
fonts/package-lock.json
generated
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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=="
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror Core.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
}
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
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");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
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");
|
||||
}
|
||||
|
||||
@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-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
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");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
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");
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
13
index.html
13
index.html
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
@@ -12,11 +12,13 @@
|
||||
|
||||
<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="css/roboto.css" />
|
||||
<link rel="stylesheet" type="text/css" href="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">
|
||||
window.mmVersion = "#VERSION#";
|
||||
window.mmTestMode = "#TESTMODE#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -40,11 +42,12 @@
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="js/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>
|
||||
@@ -52,6 +55,8 @@
|
||||
<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/positions.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,3 +0,0 @@
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
32
jest.config.js
Normal file
32
jest.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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",
|
||||
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"
|
||||
};
|
||||
};
|
158
js/animateCSS.js
Normal file
158
js/animateCSS.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* 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");
|
||||
}
|
||||
if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut };
|
333
js/app.js
333
js/app.js
@@ -1,33 +1,38 @@
|
||||
/* Magic Mirror
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const envsub = require("envsub");
|
||||
const Log = require("logger");
|
||||
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
|
||||
|
||||
// used to control fetch timeout for node_helpers
|
||||
const { setGlobalDispatcher, Agent } = require("undici");
|
||||
// common timeout value, provide environment override in case
|
||||
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
global.mmTestMode = process.env.mmTestMode === "true";
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// Log system information.
|
||||
Utils.logSystemInformation();
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
// FIXME: Hotfix Pull Request
|
||||
// https://github.com/MichMich/MagicMirror/pull/673
|
||||
// https://github.com/MagicMirrorOrg/MagicMirror/pull/673
|
||||
if (process.env.MM_PORT) {
|
||||
global.mmPort = process.env.MM_PORT;
|
||||
}
|
||||
@@ -35,100 +40,192 @@ if (process.env.MM_PORT) {
|
||||
// The next part is here to prevent a major exception when there
|
||||
// is no internet connection. This could probable be solved better.
|
||||
process.on("uncaughtException", function (err) {
|
||||
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("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
// ignore strange exceptions under aarch64 coming from systeminformation:
|
||||
if (!err.stack.includes("node_modules/systeminformation")) {
|
||||
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("If you think this really is an issue, please open an issue on GitHub: https://github.com/MagicMirrorOrg/MagicMirror/issues");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
function App() {
|
||||
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`);
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
defaults.address = "0.0.0.0";
|
||||
}
|
||||
|
||||
// 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.constants.F_OK);
|
||||
} catch (err) {
|
||||
templateFile = null;
|
||||
Log.log("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.log(`${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}`);
|
||||
}
|
||||
}
|
||||
|
||||
require(`${global.root_path}/js/check_config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
fs.accessSync(configFilename, fs.constants.F_OK);
|
||||
const c = require(configFilename);
|
||||
if (Object.keys(c).length === 0) {
|
||||
Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?");
|
||||
}
|
||||
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."));
|
||||
Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
|
||||
Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`);
|
||||
} else {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
Log.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) {
|
||||
function checkDeprecatedOptions (userConfig) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
// check for deprecated core options
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
||||
if (usedDeprecated.length > 0) {
|
||||
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
||||
Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
|
||||
// check for deprecated module options
|
||||
for (const element of userConfig.modules) {
|
||||
if (deprecated[element.module] !== undefined && element.config !== undefined) {
|
||||
const deprecatedModuleOptions = deprecated[element.module];
|
||||
const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option));
|
||||
if (usedDeprecatedModuleOptions.length > 0) {
|
||||
Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
const env = getEnvVarsAsObj();
|
||||
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
|
||||
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module);
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${moduleName}.js`;
|
||||
|
||||
try {
|
||||
fs.accessSync(moduleFile, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
fs.accessSync(helperPath, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
// if the helper was found
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let Module;
|
||||
try {
|
||||
Module = require(helperPath);
|
||||
} catch (e) {
|
||||
Log.error(`Error when loading ${moduleName}:`, e.message);
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
@@ -141,50 +238,33 @@ 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
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
function cmpVersions (a, b) {
|
||||
let i, diff;
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
@@ -203,43 +283,68 @@ 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);
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
let modules = [];
|
||||
// get the used module positions
|
||||
Utils.getModulePositions();
|
||||
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (module.disabled) continue;
|
||||
if (module.module) {
|
||||
if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") {
|
||||
// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module)
|
||||
if (!modules.includes(module.module)) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
httpServer = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } }));
|
||||
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
}
|
||||
await loadModules(modules);
|
||||
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
httpServer = new Server(config);
|
||||
const { app, io } = await httpServer.open();
|
||||
Log.log("Server started ...");
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
}
|
||||
});
|
||||
});
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
|
||||
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 ...");
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -247,14 +352,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) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
this.stop = async function () {
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
try {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodePromises.push(nodeHelper.stop());
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
httpServer.close();
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -264,12 +395,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);
|
||||
});
|
||||
|
||||
@@ -277,12 +408,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,27 +1,23 @@
|
||||
/* Magic Mirror
|
||||
*
|
||||
* 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 path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const { styleText } = require("node:util");
|
||||
const Ajv = require("ajv");
|
||||
const globals = require("globals");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
const linter = new Linter({ configType: "flat" });
|
||||
const ajv = new Ajv();
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
function getConfigFile () {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
|
||||
}
|
||||
@@ -29,45 +25,112 @@ function getConfigFile() {
|
||||
/**
|
||||
* Checks the config file using eslint.
|
||||
*/
|
||||
function checkConfigFile() {
|
||||
function checkConfigFile () {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
Log.error(Utils.colors.error("File not found: "), configFileName);
|
||||
throw new Error("No config file present!");
|
||||
throw new Error(`File not found: ${configFileName}\nNo config file present!`);
|
||||
}
|
||||
|
||||
// Check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.error(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
fs.accessSync(configFileName, fs.constants.F_OK);
|
||||
} catch (error) {
|
||||
throw new Error(`${error}\nNo permission to access config file!`);
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
Log.info(`Checking config file ${configFileName} ...`);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
}
|
||||
});
|
||||
const errors = linter.verify(
|
||||
configFile,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: { "no-undef": "error" }
|
||||
},
|
||||
configFileName
|
||||
);
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
|
||||
validateModulePositions(configFileName);
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
let errorMessage = "Your configuration file contains syntax errors :(";
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
/**
|
||||
*
|
||||
* @param {string} configFileName - The path and filename of the configuration file to validate.
|
||||
*/
|
||||
function validateModulePositions (configFileName) {
|
||||
Log.info("Checking modules structure configuration ...");
|
||||
|
||||
const positionList = Utils.getModulePositions();
|
||||
|
||||
// Make Ajv schema configuration of modules config
|
||||
// Only scan "module" and "position"
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
modules: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
module: {
|
||||
type: "string"
|
||||
},
|
||||
position: {
|
||||
type: "string",
|
||||
enum: positionList
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Scan all modules
|
||||
const validate = ajv.compile(schema);
|
||||
const data = require(configFileName);
|
||||
|
||||
const valid = validate(data);
|
||||
if (valid) {
|
||||
Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)"));
|
||||
} else {
|
||||
const module = validate.errors[0].instancePath.split("/")[2];
|
||||
const position = validate.errors[0].instancePath.split("/")[3];
|
||||
let errorMessage = "This module configuration contains errors:";
|
||||
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
|
||||
if (position) {
|
||||
errorMessage += `\n${position}: ${validate.errors[0].message}`;
|
||||
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
|
||||
} else {
|
||||
errorMessage += validate.errors[0].message;
|
||||
}
|
||||
Log.error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
checkConfigFile();
|
||||
} catch (error) {
|
||||
Log.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
54
js/class.js
54
js/class.js
@@ -1,6 +1,7 @@
|
||||
/* global Class, xyz */
|
||||
|
||||
/* Simple JavaScript Inheritance
|
||||
/*
|
||||
* Simple JavaScript Inheritance
|
||||
* By John Resig https://johnresig.com/
|
||||
*
|
||||
* Inspired by base2 and Prototype
|
||||
@@ -9,7 +10,7 @@
|
||||
*/
|
||||
(function () {
|
||||
let initializing = false;
|
||||
const fnTest = /xyz/.test(function () {
|
||||
const fnTest = (/xyz/).test(function () {
|
||||
xyz;
|
||||
})
|
||||
? /\b_super\b/
|
||||
@@ -22,8 +23,10 @@
|
||||
Class.extend = function (prop) {
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
/*
|
||||
* Instantiate a base class (but only create the instance,
|
||||
* don't run the init constructor)
|
||||
*/
|
||||
initializing = true;
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
@@ -36,31 +39,35 @@
|
||||
// Copy the properties over onto the new prototype
|
||||
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])
|
||||
prototype[name]
|
||||
= typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
? (function (name, fn) {
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
this._super = _super[name];
|
||||
/*
|
||||
* Add a new ._super() method that is the same method
|
||||
* but on the super-class
|
||||
*/
|
||||
this._super = _super[name];
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
/*
|
||||
* The method only need to be bound temporarily, so we
|
||||
* remove it when we're done executing
|
||||
*/
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
return ret;
|
||||
};
|
||||
})(name, prop[name])
|
||||
return ret;
|
||||
};
|
||||
}(name, prop[name]))
|
||||
: prop[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* The dummy class constructor
|
||||
*/
|
||||
function Class() {
|
||||
function Class () {
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing && this.init) {
|
||||
this.init.apply(this, arguments);
|
||||
@@ -78,19 +85,22 @@
|
||||
|
||||
return Class;
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
function cloneObject (obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj.constructor.name === "RegExp") {
|
||||
return new RegExp(obj);
|
||||
}
|
||||
|
||||
const temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (const key in obj) {
|
||||
temp[key] = cloneObject(obj[key]);
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global mmPort */
|
||||
|
||||
/* Magic Mirror
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
@@ -25,6 +19,15 @@ const defaults = {
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
foreignModulesDir: "modules",
|
||||
// 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/MagicMirrorOrg/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 +39,7 @@ const defaults = {
|
||||
position: "upper_third",
|
||||
classes: "large thin",
|
||||
config: {
|
||||
text: "Magic Mirror<sup>2</sup>"
|
||||
text: "MagicMirror²"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -59,7 +62,7 @@ const 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>node --run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67,15 +70,10 @@ const defaults = {
|
||||
position: "bottom_bar",
|
||||
classes: "xsmall dimmed",
|
||||
config: {
|
||||
text: "www.michaelteeuw.nl"
|
||||
text: "https://magicmirror.builders/"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
paths: {
|
||||
modules: "modules",
|
||||
vendor: "vendor"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -1,11 +1,4 @@
|
||||
/* Magic Mirror Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
configs: ["kioskmode"],
|
||||
clock: ["secondsColor"]
|
||||
};
|
||||
|
152
js/electron.js
152
js/electron.js
@@ -1,29 +1,54 @@
|
||||
"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;
|
||||
|
||||
/*
|
||||
* Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
* you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
* See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
*/
|
||||
if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
/*
|
||||
* Keep a global reference of the window object, if you don't, the window will
|
||||
* be closed automatically when the JavaScript object is garbage collected.
|
||||
*/
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
function createWindow () {
|
||||
|
||||
/*
|
||||
* 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,
|
||||
icon: "mm2.png",
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
@@ -35,34 +60,59 @@ function createWindow() {
|
||||
backgroundColor: "#000000"
|
||||
};
|
||||
|
||||
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
// settings these options directly instead provides cleaner interface
|
||||
/*
|
||||
* DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
* settings these options directly instead provides cleaner interface
|
||||
*/
|
||||
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);
|
||||
|
||||
if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) {
|
||||
// if we are running with jest and we want to mock the current date
|
||||
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
|
||||
Date = class extends Date {
|
||||
constructor (...args) {
|
||||
if (args.length === 0) {
|
||||
super(fakeNow);
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
const __DateNowOffset = fakeNow - Date.now();
|
||||
const __DateNow = Date.now;
|
||||
Date.now = () => __DateNow() + __DateNowOffset;
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
/*
|
||||
* and load the index.html of the app.
|
||||
* 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://";
|
||||
}
|
||||
|
||||
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
|
||||
let address = (config.address === void 0) | (config.address === "") | (config.address === "0.0.0.0") ? (config.address = "localhost") : config.address;
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
mainWindow.loadURL(`${prefix}${address}:${port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
// Open the DevTools if run with "node --run start:dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
@@ -97,14 +147,25 @@ function createWindow() {
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
//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])));
|
||||
}
|
||||
|
||||
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 () {
|
||||
@@ -117,33 +178,58 @@ app.on("window-all-closed", function () {
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
/*
|
||||
* On OS X it's common to re-create a window in the app when the
|
||||
* dock icon is clicked and there are no other windows open.
|
||||
*/
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
/* This method will be called when SIGINT is received and will call
|
||||
/*
|
||||
* This method will be called when SIGINT is received and will call
|
||||
* each node_helper's stop function if it exists. Added to fix #1056
|
||||
*
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||
*/
|
||||
app.on("before-quit", (event) => {
|
||||
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);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
config = c;
|
||||
/**
|
||||
* 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().then((c) => {
|
||||
config = c;
|
||||
app.whenReady().then(() => {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
255
js/loader.js
255
js/loader.js
@@ -1,12 +1,7 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Loader = (function () {
|
||||
|
||||
/* Create helper variables */
|
||||
|
||||
const loadedModuleFiles = [];
|
||||
@@ -16,48 +11,44 @@ const Loader = (function () {
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
* Retrieve object of env variables.
|
||||
* @returns {object} with key: values as assembled in js/server_functions.js
|
||||
*/
|
||||
const loadModules = function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
const loadNextModule = function () {
|
||||
if (moduleData.length > 0) {
|
||||
const 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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadNextModule();
|
||||
const getEnvVars = async function () {
|
||||
const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`);
|
||||
return JSON.parse(await res.text());
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
const startModules = function () {
|
||||
const startModules = async function () {
|
||||
const modulePromises = [];
|
||||
for (const module of moduleObjects) {
|
||||
module.start();
|
||||
try {
|
||||
modulePromises.push(module.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${module.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
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 (const thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
Log.info(`Initially hiding ${thisModule.name}`);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
@@ -65,31 +56,39 @@ const Loader = (function () {
|
||||
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined"));
|
||||
return AllModules;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
const getModuleData = function () {
|
||||
const getModuleData = async function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
const envVars = await getEnvVars();
|
||||
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = config.paths.modules + "/" + module;
|
||||
let moduleFolder = `${envVars.modulesDir}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = config.paths.modules + "/default/" + module;
|
||||
const defaultModuleFolder = `modules/default/${module}`;
|
||||
if (window.name !== "jsdom") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (envVars.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
@@ -98,16 +97,19 @@ const Loader = (function () {
|
||||
|
||||
moduleFiles.push({
|
||||
index: index,
|
||||
identifier: "module_" + index + "_" + module,
|
||||
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,
|
||||
order: (typeof moduleData.order === "number" && Number.isInteger(moduleData.order)) ? moduleData.order : 0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,134 +117,135 @@ const Loader = (function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const loadModule = function (module, callback) {
|
||||
const loadModule = async function (module) {
|
||||
const url = module.path + module.file;
|
||||
|
||||
const afterLoad = function () {
|
||||
/**
|
||||
* @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 () {
|
||||
loadedModuleFiles.push(url);
|
||||
afterLoad();
|
||||
});
|
||||
await loadFile(url);
|
||||
loadedModuleFiles.push(url);
|
||||
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.
|
||||
*/
|
||||
const 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);
|
||||
moduleObjects.push(mObj);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const loadFile = function (fileName, callback) {
|
||||
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);
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
break;
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load script: ${fileName}`);
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
});
|
||||
case "css":
|
||||
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();
|
||||
}
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
Log.log(`Load stylesheet: ${fileName}`);
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.type = "text/css";
|
||||
stylesheet.href = fileName;
|
||||
stylesheet.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
stylesheet.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* Public Methods */
|
||||
return {
|
||||
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
async loadModules () {
|
||||
const moduleData = await getModuleData();
|
||||
const envVars = await getEnvVars();
|
||||
const customCss = envVars.customCss;
|
||||
|
||||
// Load all modules
|
||||
for (const module of moduleData) {
|
||||
await loadModule(module);
|
||||
}
|
||||
|
||||
// Load custom.css
|
||||
// Since this happens after loading the modules,
|
||||
// it overwrites the default styles.
|
||||
await loadFile(customCss);
|
||||
|
||||
// Start all modules.
|
||||
await startModules();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
async loadFileForModule (fileName, module) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
Log.log("File already loaded: " + fileName);
|
||||
callback();
|
||||
Log.log(`File already loaded: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,22 +253,20 @@ const 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(`${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));
|
||||
}
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
80
js/logger.js
80
js/logger.js
@@ -1,19 +1,49 @@
|
||||
/* Magic Mirror
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
* This system can eventually be used to push the log messages to an external target.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// This logger is very simple, but needs to be extended.
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
const { styleText } = require("node:util");
|
||||
|
||||
// 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"]
|
||||
format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :msg",
|
||||
tokens: {
|
||||
label: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let label = defaultTokens.label(arg);
|
||||
switch (method) {
|
||||
case "error":
|
||||
label = styleText("red", label);
|
||||
break;
|
||||
case "warn":
|
||||
label = styleText("yellow", label);
|
||||
break;
|
||||
case "debug":
|
||||
label = styleText("bgBlue", label);
|
||||
break;
|
||||
case "info":
|
||||
label = styleText("blue", label);
|
||||
break;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
msg: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let msg = defaultTokens.msg(arg);
|
||||
switch (method) {
|
||||
case "error":
|
||||
msg = styleText("red", msg);
|
||||
break;
|
||||
case "warn":
|
||||
msg = styleText("yellow", msg);
|
||||
break;
|
||||
case "info":
|
||||
msg = styleText("blue", msg);
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
@@ -22,7 +52,7 @@
|
||||
// Browser globals (root is window)
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
}(this, function (config) {
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
@@ -50,7 +80,7 @@
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
Object.keys(logLevel).forEach(function (key) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
@@ -59,21 +89,21 @@
|
||||
};
|
||||
} else {
|
||||
logLevel = {
|
||||
debug: function () {},
|
||||
log: function () {},
|
||||
info: function () {},
|
||||
warn: function () {},
|
||||
error: function () {},
|
||||
group: function () {},
|
||||
groupCollapsed: function () {},
|
||||
groupEnd: function () {},
|
||||
time: function () {},
|
||||
timeEnd: function () {},
|
||||
timeStamp: function () {}
|
||||
debug () {},
|
||||
log () {},
|
||||
info () {},
|
||||
warn () {},
|
||||
error () {},
|
||||
group () {},
|
||||
groupCollapsed () {},
|
||||
groupEnd () {},
|
||||
time () {},
|
||||
timeEnd () {},
|
||||
timeStamp () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function () {};
|
||||
}
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
}));
|
||||
|
303
js/main.js
303
js/main.js
@@ -1,11 +1,5 @@
|
||||
/* global Loader, defaults, Translator */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
|
||||
|
||||
/* Magic Mirror
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
@@ -22,6 +16,10 @@ const MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const wrapper = selectWrapper(module.data.position);
|
||||
|
||||
const dom = document.createElement("div");
|
||||
@@ -29,9 +27,11 @@ const MM = (function () {
|
||||
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.style.order = (typeof module.data.order === "number" && Number.isInteger(module.data.order)) ? module.data.order : 0;
|
||||
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
@@ -50,7 +50,12 @@ const MM = (function () {
|
||||
moduleContent.className = "module-content";
|
||||
dom.appendChild(moduleContent);
|
||||
|
||||
const 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,9 +73,8 @@ const 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
|
||||
*/
|
||||
const selectWrapper = function (position) {
|
||||
const classes = position.replace("_", " ");
|
||||
@@ -85,7 +89,6 @@ const 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.
|
||||
@@ -102,13 +105,31 @@ const 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.
|
||||
*/
|
||||
const updateDom = function (module, speed) {
|
||||
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
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();
|
||||
|
||||
@@ -119,7 +140,7 @@ const MM = (function () {
|
||||
|
||||
newContentPromise
|
||||
.then(function (newContent) {
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
|
||||
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
})
|
||||
@@ -129,14 +150,16 @@ const 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.
|
||||
*/
|
||||
const 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);
|
||||
@@ -155,19 +178,33 @@ const 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.
|
||||
@@ -198,7 +235,6 @@ const 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.
|
||||
@@ -224,15 +260,12 @@ const 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.
|
||||
*/
|
||||
const 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);
|
||||
@@ -243,23 +276,65 @@ const MM = (function () {
|
||||
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
// 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 AnimateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finally 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;
|
||||
|
||||
updateWrapperStates();
|
||||
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;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
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
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") {
|
||||
@@ -270,15 +345,12 @@ const 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.
|
||||
*/
|
||||
const showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
const showModule = function (module, speed, callback, options = {}) {
|
||||
// remove lockString if set in options.
|
||||
if (options.lockString) {
|
||||
const index = module.lockStrings.indexOf(options.lockString);
|
||||
@@ -287,29 +359,52 @@ const MM = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// 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 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 = [];
|
||||
}
|
||||
|
||||
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 AnimateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finally 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();
|
||||
|
||||
@@ -317,12 +412,27 @@ const MM = (function () {
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
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") {
|
||||
@@ -342,10 +452,9 @@ const MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
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) {
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
@@ -356,7 +465,8 @@ const MM = (function () {
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.style.display = showWrapper ? "block" : "none";
|
||||
// move container definitions to main CSS
|
||||
wrapper.className = showWrapper ? "container" : "container hidden";
|
||||
});
|
||||
};
|
||||
|
||||
@@ -365,7 +475,6 @@ const MM = (function () {
|
||||
*/
|
||||
const loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
/* eslint-disable */
|
||||
if (typeof config === "undefined") {
|
||||
config = defaults;
|
||||
Log.error("Config file is missing! Please create a config file.");
|
||||
@@ -373,18 +482,16 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
config = Object.assign({}, defaults, config);
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* @param {Module[]} modules Array of 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.
|
||||
*/
|
||||
@@ -394,7 +501,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
@@ -404,7 +510,6 @@ const MM = (function () {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -433,7 +538,6 @@ const 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.
|
||||
*/
|
||||
@@ -448,7 +552,6 @@ const 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.
|
||||
*/
|
||||
const enumerate = function (callback) {
|
||||
@@ -472,44 +575,64 @@ const MM = (function () {
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
/* Public Methods */
|
||||
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
async init () {
|
||||
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) {
|
||||
modulesStarted (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}${config.basePath}startup`);
|
||||
const curr = await res.text();
|
||||
if (startUp === "") startUp = curr;
|
||||
if (startUp !== curr) {
|
||||
startUp = "";
|
||||
window.location.reload(true);
|
||||
Log.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.
|
||||
*/
|
||||
sendNotification: function (notification, payload, sender) {
|
||||
sendNotification (notification, payload, sender) {
|
||||
if (arguments.length < 3) {
|
||||
Log.error("sendNotification: Missing arguments.");
|
||||
return;
|
||||
@@ -531,11 +654,10 @@ const 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 (module, updateOptions) {
|
||||
if (!(module instanceof Module)) {
|
||||
Log.error("updateDom: Sender should be a module.");
|
||||
return;
|
||||
@@ -547,46 +669,49 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
updateDom(module, updateOptions).then(function () {
|
||||
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
|
||||
sendNotification("MODULE_DOM_UPDATED", null, null, module);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
getModules () {
|
||||
setSelectionMethodsForModules(modules);
|
||||
return modules;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
hideModule: function (module, speed, callback, options) {
|
||||
hideModule (module, speed, callback, options) {
|
||||
module.hidden = true;
|
||||
hideModule(module, speed, callback, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
showModule: function (module, speed, callback, options) {
|
||||
showModule (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
showModule(module, speed, callback, options);
|
||||
}
|
||||
},
|
||||
|
||||
// Return all available module positions.
|
||||
getAvailableModulePositions: modulePositions
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
||||
// Add polyfill for Object.assign.
|
||||
if (typeof Object.assign !== "function") {
|
||||
@@ -609,7 +734,7 @@ if (typeof Object.assign !== "function") {
|
||||
}
|
||||
return output;
|
||||
};
|
||||
})();
|
||||
}());
|
||||
}
|
||||
|
||||
MM.init();
|
||||
|
233
js/module.js
233
js/module.js
@@ -1,18 +1,18 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
/*
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Module = Class.extend({
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
|
||||
// Set the minimum MagicMirror module version for this module.
|
||||
/**
|
||||
********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
********************************************************
|
||||
*/
|
||||
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
|
||||
// Module config defaults.
|
||||
@@ -21,44 +21,46 @@ const Module = Class.extend({
|
||||
// Timer reference used for showHide animation callbacks.
|
||||
showHideTimer: null,
|
||||
|
||||
// Array to store lockStrings. These strings are used to lock
|
||||
// visibility when hiding and showing module.
|
||||
/*
|
||||
* Array to store lockStrings. These strings are used to lock
|
||||
* visibility when hiding and showing module.
|
||||
*/
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjuck Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
/*
|
||||
* Storage of the nunjucks Environment,
|
||||
* This should not be referenced directly.
|
||||
* Use the nunjucksEnvironment() to get it.
|
||||
*/
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init: function () {
|
||||
init () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
async start () {
|
||||
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 () {
|
||||
getScripts () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return [];
|
||||
},
|
||||
|
||||
@@ -66,28 +68,26 @@ const 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 () {
|
||||
getTranslations () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
getDom () {
|
||||
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)) {
|
||||
if ((/^.*((\.html)|(\.njk))$/).test(template)) {
|
||||
// the template is a filename
|
||||
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
@@ -109,12 +109,11 @@ const 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 () {
|
||||
getHeader () {
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
@@ -123,31 +122,28 @@ const 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>";
|
||||
getTemplate () {
|
||||
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 () {
|
||||
getTemplateData () {
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (sender) {
|
||||
// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
|
||||
} else {
|
||||
@@ -158,10 +154,9 @@ const 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 () {
|
||||
nunjucksEnvironment () {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
@@ -180,63 +175,63 @@ const 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);
|
||||
socketNotificationReceived (notification, 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.");
|
||||
suspend () {
|
||||
Log.log(`${this.name} is suspended.`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
resume () {
|
||||
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 {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
setData (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.
|
||||
*/
|
||||
setConfig: function (config, deep) {
|
||||
setConfig (config, deep) {
|
||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
socket () {
|
||||
if (typeof this._socket === "undefined") {
|
||||
this._socket = new MMSocket(this.name);
|
||||
}
|
||||
@@ -250,62 +245,56 @@ const 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("//", "/");
|
||||
file (file) {
|
||||
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 () {
|
||||
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 () {
|
||||
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) {
|
||||
async loadDependencies (funcName) {
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
const loadNextDependency = () => {
|
||||
const loadNextDependency = async () => {
|
||||
if (dependencies.length > 0) {
|
||||
const nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, this, () => {
|
||||
dependencies = dependencies.slice(1);
|
||||
loadNextDependency();
|
||||
});
|
||||
await Loader.loadFileForModule(nextDependency, this);
|
||||
dependencies = dependencies.slice(1);
|
||||
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) {
|
||||
async loadTranslations () {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
@@ -313,7 +302,6 @@ const Module = Class.extend({
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,28 +309,24 @@ const 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, () => {
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
await Translator.load(this, translationFile, false);
|
||||
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
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.
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||
translate (key, defaultValueOrVariables, defaultValue) {
|
||||
if (typeof defaultValueOrVariables === "object") {
|
||||
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
|
||||
}
|
||||
@@ -351,90 +335,87 @@ const 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 (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.
|
||||
*/
|
||||
sendNotification: function (notification, payload) {
|
||||
sendNotification (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
sendSocketNotification (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (typeof callback === "object") {
|
||||
options = callback;
|
||||
callback = function () {};
|
||||
}
|
||||
hide (speed, callback, options = {}) {
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
if (typeof callback === "object") {
|
||||
Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!");
|
||||
usedOptions = callback;
|
||||
usedCallback = function () {};
|
||||
}
|
||||
|
||||
MM.hideModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.suspend();
|
||||
callback();
|
||||
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 () {};
|
||||
}
|
||||
show (speed, callback, options) {
|
||||
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:
|
||||
@@ -452,11 +433,10 @@ const 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) {
|
||||
function configMerge (result) {
|
||||
const stack = Array.prototype.slice.call(arguments, 1);
|
||||
let item, key;
|
||||
|
||||
@@ -498,15 +478,15 @@ Module.create = function (name) {
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -514,13 +494,12 @@ 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) {
|
||||
function cmpVersions (a, b) {
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
|
18
js/module_functions.js
Normal file
18
js/module_functions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Schedule the timer for the next update
|
||||
* @param {object} timer The timer of the module
|
||||
* @param {bigint} intervalMS interval in milliseconds
|
||||
* @param {Function} callback function to call when the timer expires
|
||||
*/
|
||||
const scheduleTimer = function (timer, intervalMS, callback) {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
let tmr = timer;
|
||||
clearTimeout(tmr);
|
||||
tmr = setTimeout(function () {
|
||||
callback();
|
||||
}, intervalMS);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { scheduleTimer };
|
@@ -1,113 +1,94 @@
|
||||
/* Magic Mirror
|
||||
* 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() {
|
||||
init () {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded(callback) {
|
||||
loaded () {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start() {
|
||||
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() {
|
||||
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) {
|
||||
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) {
|
||||
setName (name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
/* setPath(path)
|
||||
/**
|
||||
* Set the module path.
|
||||
*
|
||||
* argument path string - Module path.
|
||||
* @param {string} path Module path.
|
||||
*/
|
||||
setPath(path) {
|
||||
setPath (path) {
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/*
|
||||
* sendSocketNotification(notification, payload)
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification(notification, payload) {
|
||||
sendSocketNotification (notification, payload) {
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
/* setExpressApp(app)
|
||||
/*
|
||||
* setExpressApp(app)
|
||||
* Sets the express app object for this module.
|
||||
* This allows you to host files from the created webserver.
|
||||
*
|
||||
* argument app Express app - The Express app object.
|
||||
*/
|
||||
setExpressApp(app) {
|
||||
setExpressApp (app) {
|
||||
this.expressApp = app;
|
||||
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
/*
|
||||
* setSocketIO(io)
|
||||
* Sets the socket io object for this module.
|
||||
* Binds message receiver.
|
||||
*
|
||||
* argument io Socket.io - The Socket io object.
|
||||
*/
|
||||
setSocketIO(io) {
|
||||
setSocketIO (io) {
|
||||
this.io = io;
|
||||
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
socket.onAny((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -125,7 +106,6 @@ NodeHelper.checkFetchStatus = function (response) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
189
js/server.js
189
js/server.js
@@ -1,111 +1,120 @@
|
||||
/* Magic Mirror
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const https = require("node:https");
|
||||
const path = require("node:path");
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
|
||||
const socketio = require("socket.io");
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
|
||||
|
||||
const vendor = require(`${__dirname}/vendor`);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (config.useHttps) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
server = require("https").Server(options, app);
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
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) {
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
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) {
|
||||
return next();
|
||||
/**
|
||||
* 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 = https.Server(options, app);
|
||||
} else {
|
||||
server = http.Server(app);
|
||||
}
|
||||
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.");
|
||||
const io = socketio(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
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) {
|
||||
Log.warn("You're using a full whitelist configuration to allow for all IPs");
|
||||
}
|
||||
|
||||
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(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
let directories = ["/config", "/css", "/modules", "/node_modules/animate.css", "/node_modules/@fontsource", "/node_modules/@fortawesome", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const [key, value] of Object.entries(vendor)) {
|
||||
const dirArr = value.split("/");
|
||||
if (dirArr[0] === "node_modules") directories.push(`/${dirArr[0]}/${dirArr[1]}`);
|
||||
}
|
||||
const uniqDirs = [...new Set(directories)];
|
||||
for (const directory of uniqDirs) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
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("/env", (req, res) => getEnvVars(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
resolve({
|
||||
app,
|
||||
io
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
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("/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;
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close();
|
||||
return new Promise((resolve) => {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close(resolve);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
158
js/server_functions.js
Normal file
158
js/server_functions.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node: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 expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);
|
||||
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedReceivedHeaders) {
|
||||
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 geExpectedReceivedHeaders (url) {
|
||||
const expectedReceivedHeaders = ["Content-Type"];
|
||||
const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedReceivedHeadersMatch) {
|
||||
const headers = expectedReceivedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedReceivedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedReceivedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
html = html.replace("#TESTMODE#", global.mmTestMode);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @returns {object} environment variables key: values
|
||||
*/
|
||||
function getEnvVarsAsObj () {
|
||||
const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` };
|
||||
if (process.env.MM_MODULES_DIR) {
|
||||
obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
if (process.env.MM_CUSTOMCSS_FILE) {
|
||||
obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getEnvVars (req, res) {
|
||||
const obj = getEnvVarsAsObj();
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };
|
@@ -1,11 +1,5 @@
|
||||
/* global io */
|
||||
|
||||
/* Magic Mirror
|
||||
* TODO add description
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MMSocket = function (moduleName) {
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
@@ -18,8 +12,8 @@ const MMSocket = function (moduleName) {
|
||||
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
|
||||
base = config.basePath;
|
||||
}
|
||||
this.socket = io("/" + this.moduleName, {
|
||||
path: base + "socket.io"
|
||||
this.socket = io(`/${this.moduleName}`, {
|
||||
path: `${base}socket.io`
|
||||
});
|
||||
|
||||
let notificationCallback = function () {};
|
||||
@@ -44,10 +38,7 @@ const MMSocket = function (moduleName) {
|
||||
notificationCallback = callback;
|
||||
};
|
||||
|
||||
this.sendNotification = (notification, payload) => {
|
||||
if (typeof payload === "undefined") {
|
||||
payload = {};
|
||||
}
|
||||
this.sendNotification = (notification, payload = {}) => {
|
||||
this.socket.emit(notification, payload);
|
||||
};
|
||||
};
|
||||
|
@@ -1,36 +1,32 @@
|
||||
/* global translations */
|
||||
|
||||
/* Magic Mirror
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
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();
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(" loading json file =" + file + " failed");
|
||||
return new Promise(function (resolve) {
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileInfo = null;
|
||||
try {
|
||||
fileInfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
resolve(fileInfo);
|
||||
}
|
||||
callback(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -41,34 +37,32 @@ const 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 (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
|
||||
*/
|
||||
function createStringFromTemplate(template, variables) {
|
||||
function createStringFromTemplate (template, variables) {
|
||||
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,59 +91,49 @@ const 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 property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
});
|
||||
const json = await loadJSON(module.file(file));
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
async loadCoreTranslations (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 () {
|
||||
async loadCoreTranslationsFallback () {
|
||||
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;
|
||||
|
91
js/utils.js
91
js/utils.js
@@ -1,16 +1,83 @@
|
||||
/* Magic Mirror
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const colors = require("colors/safe");
|
||||
const execSync = require("node:child_process").execSync;
|
||||
const path = require("node:path");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const si = require("systeminformation");
|
||||
|
||||
const modulePositions = []; // will get list from index.html
|
||||
const regionRegEx = /"region ([^"]*)/i;
|
||||
const indexFileName = "index.html";
|
||||
const discoveredPositionsJSFilename = "js/positions.js";
|
||||
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
|
||||
async logSystemInformation () {
|
||||
try {
|
||||
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
|
||||
|
||||
const staticData = await si.get({
|
||||
system: "manufacturer, model, virtual",
|
||||
osInfo: "platform, distro, release, arch",
|
||||
versions: "kernel, node, npm, pm2"
|
||||
});
|
||||
let systemDataString = [
|
||||
"\n##### System Information #####",
|
||||
`- SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}; timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`,
|
||||
`- OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}`,
|
||||
`- VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}`,
|
||||
`- ENV: XDG_SESSION_TYPE: ${process.env.XDG_SESSION_TYPE}; MM_CONFIG_FILE: ${process.env.MM_CONFIG_FILE};`,
|
||||
` WAYLAND_DISPLAY: ${process.env.WAYLAND_DISPLAY}; DISPLAY: ${process.env.DISPLAY}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`,
|
||||
`- RAM: total: ${(os.totalmem() / 1024 / 1024).toFixed(2)} MB; free: ${(os.freemem() / 1024 / 1024).toFixed(2)} MB; used: ${((os.totalmem() - os.freemem()) / 1024 / 1024).toFixed(2)} MB`,
|
||||
`- UPTIME: ${Math.floor(os.uptime() / 60)} minutes`
|
||||
].join("\n");
|
||||
Log.info(systemDataString);
|
||||
|
||||
// Return is currently only for jest
|
||||
return systemDataString;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return modulePositions;
|
||||
},
|
||||
|
||||
// return if position is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
getModulePositions () {
|
||||
// if not already discovered
|
||||
if (modulePositions.length === 0) {
|
||||
// get the lines of the index.html
|
||||
const lines = fs.readFileSync(indexFileName).toString().split("\n");
|
||||
// loop thru the lines
|
||||
lines.forEach((line) => {
|
||||
// run the regex on each line
|
||||
const results = regionRegEx.exec(line);
|
||||
// if the regex returned something
|
||||
if (results && results.length > 0) {
|
||||
// get the position parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
}
|
||||
catch (error) {
|
||||
Log.error("unable to write js/positions.js with the discovered module positions\nmake the MagicMirror/js folder writeable by the user starting MagicMirror");
|
||||
}
|
||||
}
|
||||
// return the list to the caller
|
||||
return modulePositions;
|
||||
}
|
||||
};
|
||||
|
9
vendor/vendor.js → js/vendor.js
Executable file → Normal file
9
vendor/vendor.js → js/vendor.js
Executable file → Normal file
@@ -1,9 +1,3 @@
|
||||
/* Magic Mirror
|
||||
* Vendor File Definition
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const vendor = {
|
||||
"moment.js": "node_modules/moment/min/moment-with-locales.js",
|
||||
"moment-timezone.js": "node_modules/moment-timezone/builds/moment-timezone-with-data.js",
|
||||
@@ -11,7 +5,8 @@ const vendor = {
|
||||
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
|
||||
"font-awesome.css": "css/font-awesome.css",
|
||||
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js"
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js",
|
||||
"croner.js": "node_modules/croner/dist/croner.umd.js"
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
@@ -1,16 +1,19 @@
|
||||
type ModuleProperties = {
|
||||
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;
|
||||
getDom?(): HTMLElement;
|
||||
getStyles?(): string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
|
@@ -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,167 +1,142 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* Magic Mirror
|
||||
* 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",
|
||||
eo: "translations/eo.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);
|
||||
|
||||
//If module already has an open alert close it
|
||||
async showAlert (alert, sender) {
|
||||
// If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender, false);
|
||||
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 />";
|
||||
}
|
||||
|
||||
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||
// Add overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(true);
|
||||
}
|
||||
|
||||
//Store alert in this.alerts
|
||||
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"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
// Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
// Add timer to dismiss alert and overlay
|
||||
if (alert.timer) {
|
||||
setTimeout(() => {
|
||||
this.hide_alert(sender);
|
||||
}, params.timer);
|
||||
this.hideAlert(sender);
|
||||
}, alert.timer);
|
||||
}
|
||||
},
|
||||
hide_alert: function (sender, close = true) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
|
||||
hideAlert (sender, close = true) {
|
||||
// Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
},
|
||||
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;}";
|
||||
}
|
||||
if (pos === "right") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";
|
||||
}
|
||||
if (pos === "left") {
|
||||
sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";
|
||||
}
|
||||
document.body.appendChild(sheet);
|
||||
},
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") {
|
||||
payload.type = "alert";
|
||||
}
|
||||
if (payload.type === "alert") {
|
||||
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 });
|
||||
delete this.alerts[sender.name];
|
||||
// Remove overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(false);
|
||||
}
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleBlur (add = false) {
|
||||
const method = add ? "add" : "remove";
|
||||
const modules = document.querySelectorAll(".module");
|
||||
for (const module of modules) {
|
||||
module.classList[method]("alert-blur");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -9,16 +9,17 @@
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
function extend(a, b) {
|
||||
function extend (a, b) {
|
||||
for (let key in b) {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
@@ -29,11 +30,10 @@
|
||||
|
||||
/**
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
function NotificationFx(options) {
|
||||
function NotificationFx (options) {
|
||||
this.options = extend({}, this.options);
|
||||
extend(this.options, options);
|
||||
this._init();
|
||||
@@ -64,10 +64,10 @@
|
||||
ttl: 6000,
|
||||
al_no: "ns-box",
|
||||
// callbacks
|
||||
onClose: function () {
|
||||
onClose () {
|
||||
return false;
|
||||
},
|
||||
onOpen: function () {
|
||||
onOpen () {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -78,8 +78,8 @@
|
||||
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;
|
||||
let strinner = '<div class="ns-box-inner">';
|
||||
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>";
|
||||
this.ntf.innerHTML = strinner;
|
||||
@@ -122,7 +122,6 @@
|
||||
|
||||
/**
|
||||
* Dismiss the notification
|
||||
*
|
||||
* @param {boolean} [close] call the onClose callback at the end
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function (close = true) {
|
||||
@@ -155,4 +154,4 @@
|
||||
* Add to global namespace
|
||||
*/
|
||||
window.NotificationFx = NotificationFx;
|
||||
})(window);
|
||||
}(window));
|
||||
|
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,7 +1,7 @@
|
||||
/* 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;
|
||||
@@ -39,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,
|
||||
@@ -59,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);
|
||||
}
|
||||
@@ -77,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFlipBack {
|
||||
@keyframes anim-flip-back {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
@@ -89,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;
|
||||
@@ -121,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;
|
||||
@@ -145,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);
|
||||
@@ -162,7 +158,7 @@
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-name: anim-fade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@@ -174,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);
|
||||
@@ -194,7 +190,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
@keyframes anim-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -206,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);
|
||||
@@ -223,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% {
|
||||
@@ -392,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% {
|
||||
@@ -441,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% {
|
||||
@@ -604,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% {
|
||||
@@ -676,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% {
|
||||
@@ -693,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);
|
||||
}
|
||||
@@ -708,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);
|
||||
}
|
||||
@@ -790,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);
|
||||
}
|
||||
@@ -805,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);
|
||||
}
|
||||
@@ -887,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);
|
||||
}
|
||||
@@ -903,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!"
|
||||
}
|
||||
|
4
modules/default/alert/translations/el.json
Normal file
4
modules/default/alert/translations/el.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror² Ειδοποίηση",
|
||||
"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!"
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"sysTitle": "MagicMirror² Notification",
|
||||
"welcome": "Welcome, start was successful!"
|
||||
}
|
||||
|
4
modules/default/alert/translations/eo.json
Normal file
4
modules/default/alert/translations/eo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror²-sciigo",
|
||||
"welcome": "Bonvenon, lanĉo sukcesis!"
|
||||
}
|
@@ -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).
|
||||
|
@@ -14,6 +14,7 @@
|
||||
.calendar .title {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.calendar .time {
|
||||
|
767
modules/default/calendar/calendar.js
Executable file → Normal file
767
modules/default/calendar/calendar.js
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,9 @@
|
||||
/* Magic Mirror
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
const https = require("node:https");
|
||||
const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("node-fetch");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("https");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
const { scheduleTimer } = require("#module_functions");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -38,10 +31,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) {
|
||||
@@ -51,18 +43,13 @@ 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, agent: 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, agent: httpsAgent });
|
||||
}
|
||||
|
||||
fetcher
|
||||
fetch(url, { headers: headers, agent: httpsAgent })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
@@ -70,8 +57,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`);
|
||||
events = CalendarFetcherUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
@@ -79,28 +66,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
@@ -114,13 +91,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) {
|
||||
@@ -129,7 +105,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
@@ -138,7 +113,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
@@ -147,7 +121,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 () {
|
||||
|
431
modules/default/calendar/calendarfetcherutils.js
Normal file
431
modules/default/calendar/calendarfetcherutils.js
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
const CalendarFetcherUtils = {
|
||||
|
||||
/**
|
||||
* Determine based on the title of an event if it should be excluded from the list of events
|
||||
* TODO This seems like an overly complicated way to exclude events based on the title.
|
||||
* @param {object} config the global config
|
||||
* @param {string} title the title of the event
|
||||
* @returns {object} excluded: true if the event should be excluded, false otherwise
|
||||
* until: the date until the event should be excluded.
|
||||
*/
|
||||
shouldEventBeExcluded (config, title) {
|
||||
let filter = {
|
||||
excluded: false,
|
||||
until: 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) {
|
||||
filter.until = until;
|
||||
} else {
|
||||
filter.excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get local timezone.
|
||||
* This method makes it easier to test if different timezones cause problems by changing this implementation.
|
||||
* @returns {string} timezone
|
||||
*/
|
||||
getLocalTimezone () {
|
||||
return moment.tz.guess();
|
||||
},
|
||||
|
||||
/**
|
||||
* This function returns a list of moments for a recurring event.
|
||||
* @param {object} event the current event which is a recurring event
|
||||
* @param {moment.Moment} pastLocalMoment The past date to search for recurring events
|
||||
* @param {moment.Moment} futureLocalMoment The future date to search for recurring events
|
||||
* @param {number} durationInMs the duration of the event, this is used to take into account currently running events
|
||||
* @returns {moment.Moment[]} All moments for the recurring event
|
||||
*/
|
||||
getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationInMs) {
|
||||
const rule = event.rrule;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// subtract the max of the duration of this event or 1 day to find events in the past that are currently still running and should therefor be displayed.
|
||||
const oneDayInMs = 24 * 60 * 60000;
|
||||
let searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate();
|
||||
let searchToDate = futureLocalMoment.clone().add(1, "days").toDate();
|
||||
Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`);
|
||||
|
||||
// if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset
|
||||
// looks like MS Outlook sets the until time incorrectly for fullday events
|
||||
if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fixup rrule until");
|
||||
rule.options.until = moment(rule.options.until).clone().startOf("day").add(1, "day")
|
||||
.toDate();
|
||||
}
|
||||
|
||||
Log.debug("fix rrule start=", rule.options.dtstart);
|
||||
Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate);
|
||||
|
||||
Log.debug(`RRule: ${rule.toString()}`);
|
||||
rule.options.tzid = null; // RRule gets *very* confused with timezones
|
||||
|
||||
let dates = rule.between(searchFromDate, searchToDate, true, () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`);
|
||||
|
||||
// shouldn't need this anymore, as RRULE not passed junk
|
||||
dates = dates.filter((d) => {
|
||||
return JSON.stringify(d) !== "null";
|
||||
});
|
||||
|
||||
// Dates are returned in UTC timezone but with localdatetime because tzid is null.
|
||||
// So we map the date to a moment using the original timezone of the event.
|
||||
return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(CalendarFetcherUtils.getLocalTimezone(), true)));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone());
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment;
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
|
||||
const now = moment();
|
||||
const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now;
|
||||
const futureLocalMoment
|
||||
= now
|
||||
.clone()
|
||||
.startOf("day")
|
||||
.add(config.maximumNumberOfDays, "days")
|
||||
// Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
.subtract(1, "seconds");
|
||||
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
Log.debug(`title: ${title}`);
|
||||
|
||||
// Return quickly if event should be excluded.
|
||||
let { excluded, eventFilterUntil } = this.shouldEventBeExcluded(config, title);
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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, null, 2)}`);
|
||||
let eventStartMoment = eventDate(event, "start");
|
||||
let eventEndMoment;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
eventEndMoment = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
eventEndMoment = eventStartMoment.clone();
|
||||
} else {
|
||||
eventEndMoment = eventStartMoment.clone().add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${eventStartMoment.toDate()}`);
|
||||
Log.debug(`end:: ${eventEndMoment.toDate()}`);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf();
|
||||
Log.debug(`duration: ${durationMs}`);
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
// TODO This should be a seperate function.
|
||||
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
|
||||
// Recurring event.
|
||||
let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
|
||||
|
||||
// Loop through the set of moment entries to see which recurrences should be added to our event list.
|
||||
// TODO This should create an event per moment so we can change anything we want.
|
||||
for (let m in moments) {
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone();
|
||||
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
|
||||
|
||||
let dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
|
||||
|
||||
Log.debug("event date dateKey=", dateKey);
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined) {
|
||||
Log.debug("have recurrences=", curEvent.recurrences);
|
||||
if (curEvent.recurrences[dateKey] !== undefined) {
|
||||
Log.debug("have a recurrence match for dateKey=", dateKey);
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
// Some event start/end dates don't have timezones
|
||||
if (curEvent.start.tz) {
|
||||
recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
} else {
|
||||
recurringEventStartMoment = moment(curEvent.start).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
}
|
||||
if (curEvent.end.tz) {
|
||||
recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
} else {
|
||||
recurringEventEndMoment = moment(curEvent.end).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
}
|
||||
} else {
|
||||
Log.debug("recurrence key ", dateKey, " doesn't match");
|
||||
}
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
if (curEvent.exdate !== undefined) {
|
||||
Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate);
|
||||
if (curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) {
|
||||
recurringEventEndMoment = recurringEventEndMoment.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 (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${recurrenceTitle}`);
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: recurringEventStartMoment.format("x"),
|
||||
endDate: recurringEventEndMoment.format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
} else {
|
||||
Log.debug("not saving event ", recurrenceTitle, eventStartMoment);
|
||||
}
|
||||
Log.debug(" ");
|
||||
}
|
||||
// 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 && eventStartMoment.valueOf() === eventEndMoment.valueOf()) {
|
||||
eventEndMoment = eventEndMoment.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (eventEndMoment < pastLocalMoment) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && eventEndMoment < now) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && eventEndMoment <= now.startOf("day")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (eventStartMoment > futureLocalMoment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: eventStartMoment.format("x"),
|
||||
endDate: eventEndMoment.format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
recurringEvent: false,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (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 (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 {moment.Moment} now Date object using previously created object for consistency
|
||||
* @param {moment.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 (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;
|
||||
}
|
||||
|
||||
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 (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,620 +1,125 @@
|
||||
/* Magic Mirror
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* 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 the event which needs adjustement
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @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 (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
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]) => {
|
||||
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();
|
||||
getLocaleSpecification (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") {
|
||||
Log.debug("\nEvent: " + JSON.stringify(event));
|
||||
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 (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
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("startDate (local): " + startDate.toDate());
|
||||
Log.debug("endDate (local): " + 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 = CalendarUtils.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 (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)) {
|
||||
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);
|
||||
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();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
temp += `${word}<br>`;
|
||||
}
|
||||
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;
|
||||
|
||||
// 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 (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
// Remove the offset, independently of the comparison between the date hour and the offset,
|
||||
// since in the case that *date houre < offset*, the *new Date* command will handle this by
|
||||
// representing the day before.
|
||||
|
||||
// Reduce the time by the offset:
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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:
|
||||
// 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);
|
||||
Log.debug("Corrected startDate (local): " + startDate.toDate());
|
||||
|
||||
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
|
||||
});
|
||||
currentLine = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 {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) {
|
||||
// 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);
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return `${string.trim().slice(0, maxLength)}…`;
|
||||
} else {
|
||||
return string.trim();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace object definition of parts to be replaced in the title
|
||||
* object definition:
|
||||
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
|
||||
* replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
|
||||
* yearmatchgroup: {number,optional} match group for year element
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform (title, titleReplace) {
|
||||
let transformedTitle = title;
|
||||
for (let tr in titleReplace) {
|
||||
let transform = titleReplace[tr];
|
||||
if (typeof transform === "object") {
|
||||
if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") {
|
||||
let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/);
|
||||
let needle = new RegExp(transform.search, "g");
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp with flags.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
let replacement = transform.replace;
|
||||
if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") {
|
||||
const yearmatch = [...title.matchAll(needle)];
|
||||
if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) {
|
||||
let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1;
|
||||
let searchstr = `$${transform.yearmatchgroup}`;
|
||||
replacement = replacement.replace(searchstr, calcage);
|
||||
}
|
||||
}
|
||||
transformedTitle = transformedTitle.replace(needle, replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
return transformedTitle;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,14 +1,12 @@
|
||||
/* CalendarFetcher Tester
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
* of starting the MagicMirror² core. Adjust the values below to your desire.
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
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)
|
||||
|
@@ -1,31 +1,32 @@
|
||||
/* Magic Mirror
|
||||
* 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);
|
||||
start () {
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (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.
|
||||
@@ -36,7 +37,7 @@ module.exports = NodeHelper.create({
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @param {string} identifier ID of the module
|
||||
*/
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
@@ -46,9 +47,14 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
let fetchIntervalCorrected;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
if (fetchInterval < 60000) {
|
||||
Log.warn(`fetchInterval for url ${url} must be >= 60000`);
|
||||
fetchIntervalCorrected = 60000;
|
||||
}
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
@@ -65,7 +71,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendarfetcher for url: " + url);
|
||||
Log.log(`Use existing calendarfetcher for url: ${url}`);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
@@ -78,7 +84,7 @@ module.exports = NodeHelper.create({
|
||||
* @param {object} fetcher the fetcher associated with the calendar
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents: function (fetcher, identifier) {
|
||||
broadcastEvents (fetcher, identifier) {
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
|
@@ -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).
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user