mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-12 09:52:37 +00:00
Compare commits
1299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
222a5f3779 | ||
|
|
72f7106086 | ||
|
|
33387b60cc | ||
|
|
83cc18f648 | ||
|
|
5a4fbbf48a | ||
|
|
f7465679c0 | ||
|
|
2a5299ebcb | ||
|
|
b3bddb2c99 | ||
|
|
997aec8cc2 | ||
|
|
c67320f185 | ||
|
|
8224a6ac35 | ||
|
|
abcee8aa56 | ||
|
|
23c6b44921 | ||
|
|
1034171e91 | ||
|
|
bf49f79e6e | ||
|
|
f7f24dbdfe | ||
|
|
31ec848aec | ||
|
|
9eb08420b6 | ||
|
|
eb6d8d4f83 | ||
|
|
e10f620cf9 | ||
|
|
f750436b64 | ||
|
|
a4a8504558 | ||
|
|
385e5aabaa | ||
|
|
d831315e20 | ||
|
|
e0906f3462 | ||
|
|
6595b6a44f | ||
|
|
0183d7a080 | ||
|
|
89e803ee42 | ||
|
|
c0ce52abe3 | ||
|
|
a1c7f20990 | ||
|
|
0ef6f89d44 | ||
|
|
54b04962a8 | ||
|
|
60f8de282d | ||
|
|
b4350278a0 | ||
|
|
c3c5307624 | ||
|
|
879d585f2e | ||
|
|
9969fede35 | ||
|
|
c15b31b374 | ||
|
|
a3a6c33b32 | ||
|
|
236bf6e0fc | ||
|
|
974de179e0 | ||
|
|
60e03777f3 | ||
|
|
05f6b2510f | ||
|
|
f3274977f5 | ||
|
|
cf7fb1a3b9 | ||
|
|
12457a87d4 | ||
|
|
9a8de7db80 | ||
|
|
68e02a528e | ||
|
|
277055f44e | ||
|
|
c3fc745c7e | ||
|
|
8901ed219d | ||
|
|
d7c70dc021 | ||
|
|
53c789bff9 | ||
|
|
eb63745664 | ||
|
|
91d72e48ad | ||
|
|
1dcda63192 | ||
|
|
3ea6544f77 | ||
|
|
d12a587f11 | ||
|
|
2b147bb98b | ||
|
|
6529eaaf9a | ||
|
|
a68aa148b8 | ||
|
|
98942d6f9c | ||
|
|
690efc0aff | ||
|
|
627cfa1dff | ||
|
|
99aca932db | ||
|
|
dd43f35bbe | ||
|
|
093988e136 | ||
|
|
087a472765 | ||
|
|
ce13d7f98b | ||
|
|
b1fc766908 | ||
|
|
22384342db | ||
|
|
badce5146a | ||
|
|
0bf3ff9c17 | ||
|
|
860840c367 | ||
|
|
221b6325f6 | ||
|
|
06389e35f9 | ||
|
|
a7756cec13 | ||
|
|
9ee11654a6 | ||
|
|
a273266e5e | ||
|
|
e2158716d6 | ||
|
|
c132206543 | ||
|
|
f49312ed13 | ||
|
|
a9f69f07e6 | ||
|
|
d7429a4812 | ||
|
|
be76d5ce9a | ||
|
|
f2bc10c5c0 | ||
|
|
43eb760bce | ||
|
|
a7684e3e9f | ||
|
|
8949aa3bec | ||
|
|
e40ddd4b69 | ||
|
|
17637fb1f6 | ||
|
|
f71defe958 | ||
|
|
b8d6a6da1f | ||
|
|
fbc886b21c | ||
|
|
8879fb55de | ||
|
|
ed316e8bf3 | ||
|
|
45529f7de9 | ||
|
|
dbdff38d2e | ||
|
|
21c3179e03 | ||
|
|
c05d93aed8 | ||
|
|
6225abb010 | ||
|
|
c41fff8f5c | ||
|
|
8589d9c482 | ||
|
|
7f264953af | ||
|
|
cfff2ad72b | ||
|
|
c0258b352e | ||
|
|
3e1b051ec3 | ||
|
|
b34bb87d7a | ||
|
|
83b8cc6729 | ||
|
|
878c0be727 | ||
|
|
e7f06f5c0c | ||
|
|
a1fc38c5fe | ||
|
|
ff0ab24000 | ||
|
|
56a10d192d | ||
|
|
1a8413d8f0 | ||
|
|
934b156ebb | ||
|
|
f9639d9705 | ||
|
|
4c345c4f33 | ||
|
|
490151267a | ||
|
|
3d19a08cc7 | ||
|
|
385c4c32f9 | ||
|
|
3a5052c871 | ||
|
|
f84f590f1d | ||
|
|
5b9eba7819 | ||
|
|
cd18794fca | ||
|
|
ae3d552ad7 | ||
|
|
be5f71f4a7 | ||
|
|
745a5f0376 | ||
|
|
99114b2a61 | ||
|
|
df9bd2b0f9 | ||
|
|
e194b559ac | ||
|
|
af5344dccd | ||
|
|
2d7b8121d7 | ||
|
|
0297450702 | ||
|
|
6b17f6aa28 | ||
|
|
8a7abfe42d | ||
|
|
dd5041395c | ||
|
|
36d6a5bc15 | ||
|
|
2619f92d09 | ||
|
|
53720ae8ae | ||
|
|
bcff953fbb | ||
|
|
bcc0cc599d | ||
|
|
a1e3fed312 | ||
|
|
399dca2ef9 | ||
|
|
2e44e1626d | ||
|
|
39aa2dfe01 | ||
|
|
099929c677 | ||
|
|
af5d132410 | ||
|
|
79acbc3a98 | ||
|
|
e75e4e2284 | ||
|
|
9aa0af4f9c | ||
|
|
50e272efba | ||
|
|
209e049893 | ||
|
|
bbb3accf0c | ||
|
|
179989aa42 | ||
|
|
2881d19d43 | ||
|
|
7cfc3458ec | ||
|
|
659e1da79d | ||
|
|
a7ae79493d | ||
|
|
8b484ee707 | ||
|
|
d617d4aa09 | ||
|
|
99c04648b4 | ||
|
|
f945d50c0d | ||
|
|
e9fabd59ed | ||
|
|
ad13de3588 | ||
|
|
aad8141e27 | ||
|
|
26a76f80d6 | ||
|
|
53ead2087f | ||
|
|
faee811d67 | ||
|
|
b9c739df1f | ||
|
|
6e124842e8 | ||
|
|
7a5928ea24 | ||
|
|
7fdf7de11c | ||
|
|
b75eedb84e | ||
|
|
3b92ae49a9 | ||
|
|
775d1091db | ||
|
|
1f77b491fc | ||
|
|
eff2fd7cc0 | ||
|
|
58d2a0d874 | ||
|
|
ea9def997a | ||
|
|
a222c58047 | ||
|
|
39a838c2ab | ||
|
|
cfc0bcd5ad | ||
|
|
3418c9b50f | ||
|
|
e686611890 | ||
|
|
ebb5dee1fc | ||
|
|
d9edaffd9c | ||
|
|
cbe7b1a5b9 | ||
|
|
e758fd4093 | ||
|
|
14a99a3b25 | ||
|
|
1ba67506a0 | ||
|
|
2a6ca5d5ac | ||
|
|
aa12e6495a | ||
|
|
9269848f66 | ||
|
|
a71e61cd30 | ||
|
|
8be4604c97 | ||
|
|
a7bba903f5 | ||
|
|
1d8af5835d | ||
|
|
189c01fc74 | ||
|
|
e3a5bbf661 | ||
|
|
ee23c5f72c | ||
|
|
a2083be76b | ||
|
|
a1c4be83d6 | ||
|
|
d2fde2bfc8 | ||
|
|
e8956b0b55 | ||
|
|
2af4009a93 | ||
|
|
a5f7c946cc | ||
|
|
298542b531 | ||
|
|
fca6707a29 | ||
|
|
10d3a284e9 | ||
|
|
1a244726aa | ||
|
|
044935a164 | ||
|
|
99e5edf2c5 | ||
|
|
b26270bd13 | ||
|
|
65a8cb9ddb | ||
|
|
ba4b976e80 | ||
|
|
297ae1dbaf | ||
|
|
214614f740 | ||
|
|
0e14d3d6e8 | ||
|
|
67011c0c32 | ||
|
|
16bbb42b8d | ||
|
|
b85ac91e6c | ||
|
|
66759a33fa | ||
|
|
bf5e83861c | ||
|
|
d56a6fb06f | ||
|
|
5e7aa8e16d | ||
|
|
bace0ad339 | ||
|
|
6014eaf8eb | ||
|
|
3e96e8b3f5 | ||
|
|
95d1b8a6d0 | ||
|
|
0ecb66c99e | ||
|
|
af52b91799 | ||
|
|
3c50d6c30a | ||
|
|
2722c72c43 | ||
|
|
d5ab3101c6 | ||
|
|
32df76bdff | ||
|
|
68a06c3d1d | ||
|
|
1b42dc779b | ||
|
|
cedffd40f2 | ||
|
|
cdc8db4837 | ||
|
|
a0ee23d84e | ||
|
|
49d2d8c9d0 | ||
|
|
63620aa811 | ||
|
|
d5b11a1dba | ||
|
|
4a63af0490 | ||
|
|
a68019293f | ||
|
|
6b1c91f0dd | ||
|
|
ea93785581 | ||
|
|
32819c4fd5 | ||
|
|
fc5a438cdc | ||
|
|
57fe94f945 | ||
|
|
aa3a3bdf16 | ||
|
|
db89da3daa | ||
|
|
d6ba5796ce | ||
|
|
3968743b28 | ||
|
|
20c6226b84 | ||
|
|
463ce394fe | ||
|
|
1faefebe42 | ||
|
|
e2c9339ec4 | ||
|
|
4b1c7da171 | ||
|
|
bdfd6e5e9f | ||
|
|
4c8508b0a9 | ||
|
|
06b3f92963 | ||
|
|
d43a57af36 | ||
|
|
aeefe28710 | ||
|
|
e9de961a23 | ||
|
|
dcec778e02 | ||
|
|
b212641069 | ||
|
|
90aa50bb11 | ||
|
|
a6879e853b | ||
|
|
37fab7ac63 | ||
|
|
8b01ae08c5 | ||
|
|
b1cdf42790 | ||
|
|
974968d238 | ||
|
|
bf467cbba5 | ||
|
|
536aa2e96e | ||
|
|
3d84344b75 | ||
|
|
1054ba3b1e | ||
|
|
aa8ddb9a92 | ||
|
|
fcfe57e5e2 | ||
|
|
c4fd4e0317 | ||
|
|
3c76933824 | ||
|
|
fa83819bee | ||
|
|
b65ae88879 | ||
|
|
96db21f9bf | ||
|
|
6d356ff770 | ||
|
|
21790b32bf | ||
|
|
159f3d0aa2 | ||
|
|
012a7b0678 | ||
|
|
6595c85671 | ||
|
|
cf5c0464fe | ||
|
|
e31450f731 | ||
|
|
3653984a95 | ||
|
|
0c0b856c37 | ||
|
|
69f1b153ea | ||
|
|
43ba4bd00e | ||
|
|
bf5edcaac6 | ||
|
|
ac51709211 | ||
|
|
e1a578e819 | ||
|
|
591c9e53b0 | ||
|
|
87d543eb3a | ||
|
|
40c1521591 | ||
|
|
b31c2a6264 | ||
|
|
66a42f13f1 | ||
|
|
8de6ebbbd1 | ||
|
|
649de694ed | ||
|
|
8a52fde8fc | ||
|
|
fb8bd657de | ||
|
|
b04a0a6b61 | ||
|
|
f29c911a0f | ||
|
|
bd908123c2 | ||
|
|
cbdb0b67ab | ||
|
|
aa3848f420 | ||
|
|
6cf0748172 | ||
|
|
11122d3f81 | ||
|
|
2dea9398f2 | ||
|
|
de93b3294f | ||
|
|
7accb84eb9 | ||
|
|
ea90ed04d6 | ||
|
|
838eed2630 | ||
|
|
376b65c749 | ||
|
|
3b4432cb00 | ||
|
|
7bc71029de | ||
|
|
d736dd92be | ||
|
|
6eba8d681c | ||
|
|
5fe654c19d | ||
|
|
dd366f35a8 | ||
|
|
2ababa521d | ||
|
|
bda8f26511 | ||
|
|
7f1a3df25b | ||
|
|
ef2ff50089 | ||
|
|
0b3964c827 | ||
|
|
ccf5bb9342 | ||
|
|
4303882c6a | ||
|
|
c34028d549 | ||
|
|
8e76cdcb57 | ||
|
|
5eb66106b9 | ||
|
|
0abebc1e32 | ||
|
|
552e82f44d | ||
|
|
ada40e36db | ||
|
|
480f734a06 | ||
|
|
20bee9c334 | ||
|
|
de8267f41e | ||
|
|
7bfaf07980 | ||
|
|
a42fa8e9f9 | ||
|
|
5facad683a | ||
|
|
fd1913a72e | ||
|
|
54c98b4250 | ||
|
|
5d99baac21 | ||
|
|
cb95bdf6d7 | ||
|
|
25e803abfc | ||
|
|
7c6073e4ef | ||
|
|
09bcbe8dfc | ||
|
|
e0a9c7d0bb | ||
|
|
ac27d05fd5 | ||
|
|
d6ab56252f | ||
|
|
dab178ed50 | ||
|
|
6ed50b6a75 | ||
|
|
ee559ec650 | ||
|
|
4310238418 | ||
|
|
10c47a6c38 | ||
|
|
4b8043086e | ||
|
|
fd952b88bf | ||
|
|
e262d463c5 | ||
|
|
b02bce2510 | ||
|
|
58094531c0 | ||
|
|
d937e3ca7c | ||
|
|
9c58413209 | ||
|
|
db65ff12d7 | ||
|
|
f93b819ea6 | ||
|
|
e6fea297e5 | ||
|
|
5c2a0e5634 | ||
|
|
71aa21a82d | ||
|
|
23821360c7 | ||
|
|
79f5b938f5 | ||
|
|
f8769fcc2a | ||
|
|
a3ed24c766 | ||
|
|
82727b825c | ||
|
|
16e98496af | ||
|
|
62896ce1a3 | ||
|
|
7ea5b1ecbf | ||
|
|
eecc95f8fb | ||
|
|
85808d85c4 | ||
|
|
12cc670642 | ||
|
|
13fcb55df6 | ||
|
|
a4dfd15888 | ||
|
|
7fbd326298 | ||
|
|
331d147d50 | ||
|
|
acdcdc55bc | ||
|
|
fdb0c0acb3 | ||
|
|
564bf47fb2 | ||
|
|
eca35b2371 | ||
|
|
f97d2e2644 | ||
|
|
fdd6659139 | ||
|
|
0d698fb659 | ||
|
|
c68b39dda8 | ||
|
|
cb67286bc3 | ||
|
|
a49962b8de | ||
|
|
256d5ae14f | ||
|
|
799ee8bcfa | ||
|
|
fe8a317ef9 | ||
|
|
b6d6ee45e0 | ||
|
|
0151466a28 | ||
|
|
3588875e28 | ||
|
|
e8031aec39 | ||
|
|
1cabe107e6 | ||
|
|
ba0c59744b | ||
|
|
50c702c00a | ||
|
|
ff8c2fe227 | ||
|
|
f9c78a5263 | ||
|
|
d46784a4d5 | ||
|
|
ff43d082f2 | ||
|
|
83801736d7 | ||
|
|
e16986cf71 | ||
|
|
7ad12d954c | ||
|
|
fb74fadec2 | ||
|
|
c00bdf910e | ||
|
|
50cc1c56e5 | ||
|
|
0006099758 | ||
|
|
162dcec331 | ||
|
|
3e8bd022e2 | ||
|
|
01e5936671 | ||
|
|
d85e1c70d6 | ||
|
|
f329770194 | ||
|
|
08194925cb | ||
|
|
2b7aa3e810 | ||
|
|
549417f106 | ||
|
|
f0f370cc7d | ||
|
|
b842241f8c | ||
|
|
93aa4b7440 | ||
|
|
9e2eef7818 | ||
|
|
b29b65851e | ||
|
|
a6829bff4f | ||
|
|
491f5aa776 | ||
|
|
d401e59031 | ||
|
|
63ce69ce82 | ||
|
|
f15972c823 | ||
|
|
b210fcdcf3 | ||
|
|
8ea37a5a45 | ||
|
|
daf98c36fc | ||
|
|
6a0e6eb84e | ||
|
|
7bcdd2a42c | ||
|
|
670a760404 | ||
|
|
a99de1ad13 | ||
|
|
108fe2082d | ||
|
|
e67dc38890 | ||
|
|
20a87656ff | ||
|
|
70c3b68e67 | ||
|
|
bfcb7dc601 | ||
|
|
39e16221fb | ||
|
|
d11e4e5c92 | ||
|
|
cccaa1f33d | ||
|
|
37d3aa25b1 | ||
|
|
ebfe96d31d | ||
|
|
e902b8c52f | ||
|
|
201b36d62c | ||
|
|
7b29070516 | ||
|
|
9ed2e4d557 | ||
|
|
38544f2368 | ||
|
|
df39408411 | ||
|
|
2e01f988f5 | ||
|
|
2b940c9cfb | ||
|
|
b6f4737ecc | ||
|
|
89d6473052 | ||
|
|
99d838f9cd | ||
|
|
f3bdddfaa9 | ||
|
|
87f952c87b | ||
|
|
639305ecc8 | ||
|
|
a70bb517e1 | ||
|
|
e4f671c898 | ||
|
|
a269b5cd93 | ||
|
|
2ec275957f | ||
|
|
4f9fc032e5 | ||
|
|
5c9dbccc10 | ||
|
|
b2cf470ec9 | ||
|
|
bceb181b47 | ||
|
|
1a314b741a | ||
|
|
7635dea3e9 | ||
|
|
90112d1a7d | ||
|
|
ad0cf12e53 | ||
|
|
a53029f11e | ||
|
|
848529f9f4 | ||
|
|
b5dc91fd07 | ||
|
|
0a2b939514 | ||
|
|
52584f36d7 | ||
|
|
30c7a24fc2 | ||
|
|
0643a103ac | ||
|
|
71bd45dfd4 | ||
|
|
85c9d3b331 | ||
|
|
5e6cbeb9ba | ||
|
|
3d4429d418 | ||
|
|
d3d64d3ca0 | ||
|
|
6de983aeb2 | ||
|
|
05c3a5bf83 | ||
|
|
c2f5d038de | ||
|
|
0ac5032db9 | ||
|
|
9b93066cbe | ||
|
|
bc60ae21c4 | ||
|
|
0b37ed072c | ||
|
|
57174f09b9 | ||
|
|
61057d1a25 | ||
|
|
03964d6f68 | ||
|
|
7515fc10d1 | ||
|
|
6583f05858 | ||
|
|
2b1dbbde68 | ||
|
|
49c95a9f46 | ||
|
|
a6214c8da3 | ||
|
|
d2b3414ac6 | ||
|
|
401a6f3417 | ||
|
|
3ed223a550 | ||
|
|
47bd48e0a3 | ||
|
|
cdd1853369 | ||
|
|
8f2980c23d | ||
|
|
b72556b9a9 | ||
|
|
49be3cbd6b | ||
|
|
fbb0c59b4e | ||
|
|
d83e696a8d | ||
|
|
a467d900c9 | ||
|
|
96be8d6fea | ||
|
|
8b1ce26fa6 | ||
|
|
514b9453f8 | ||
|
|
fa0f997928 | ||
|
|
504e7cadd7 | ||
|
|
c80aeff945 | ||
|
|
454206c803 | ||
|
|
e4f47178fc | ||
|
|
1f9109f8e4 | ||
|
|
f09c54184a | ||
|
|
92a35692f2 | ||
|
|
53e300bd31 | ||
|
|
e8be6ad4f1 | ||
|
|
01b9ecb339 | ||
|
|
6ff85ebe54 | ||
|
|
ea6eebd809 | ||
|
|
b2a21b937d | ||
|
|
13010ecaee | ||
|
|
2e2e157017 | ||
|
|
e23e33852e | ||
|
|
5b270b84b4 | ||
|
|
9094240024 | ||
|
|
1db0dbf52b | ||
|
|
beb5faef8b | ||
|
|
c6aff8b7dc | ||
|
|
1aacc37c83 | ||
|
|
b18d98f5ea | ||
|
|
09ddd3d925 | ||
|
|
e2b4823e43 | ||
|
|
04491ac5ac | ||
|
|
e3bee5aae7 | ||
|
|
89152e537e | ||
|
|
652e1a528f | ||
|
|
9d85baee37 | ||
|
|
bdc4ed4d86 | ||
|
|
35f3b315a2 | ||
|
|
efec49bb47 | ||
|
|
68bc77c81e | ||
|
|
dbea348779 | ||
|
|
b46160bcbc | ||
|
|
ddb06ca214 | ||
|
|
ef2fd16b69 | ||
|
|
d874ad9988 | ||
|
|
57dc349675 | ||
|
|
120f0299b2 | ||
|
|
b5b6df5e48 | ||
|
|
5ffd20b843 | ||
|
|
3dacce6675 | ||
|
|
8e4ba4fe93 | ||
|
|
37d488760f | ||
|
|
7cdeceedf1 | ||
|
|
7ba76020d8 | ||
|
|
aab1b97653 | ||
|
|
221eadcc20 | ||
|
|
4a11cdc564 | ||
|
|
b9333134c7 | ||
|
|
8c83dc9494 | ||
|
|
1ed721fb15 | ||
|
|
88ed5ed373 | ||
|
|
84995c9252 | ||
|
|
6fadc76fe3 | ||
|
|
d240986fdc | ||
|
|
afc73920ad | ||
|
|
16615c3da2 | ||
|
|
88c80973f1 | ||
|
|
e322be2624 | ||
|
|
059b87bbb4 | ||
|
|
911675f995 | ||
|
|
e76fe5e25a | ||
|
|
308774c2a6 | ||
|
|
db24f20289 | ||
|
|
94bb8e6c03 | ||
|
|
41da6f455a | ||
|
|
d2a7a3b0bb | ||
|
|
afbdacf136 | ||
|
|
2324579057 | ||
|
|
3c357f057b | ||
|
|
5116a2fc82 | ||
|
|
c0ddc020d5 | ||
|
|
0683734d5a | ||
|
|
6cbd267384 | ||
|
|
925113fa20 | ||
|
|
3696d45e94 | ||
|
|
cb3ae66652 | ||
|
|
948b6c8de8 | ||
|
|
3c4d7a33e0 | ||
|
|
5a421220c9 | ||
|
|
41508931be | ||
|
|
e2cfa24686 | ||
|
|
d48113f2d9 | ||
|
|
16c5bddbb7 | ||
|
|
ef7556f6d3 | ||
|
|
a3cb0b7b96 | ||
|
|
4d28688f30 | ||
|
|
01ff00fa31 | ||
|
|
78190d9c7f | ||
|
|
b1565e4047 | ||
|
|
db220b7861 | ||
|
|
eaf27b837e | ||
|
|
74410344af | ||
|
|
998f64f983 | ||
|
|
684dfb643b | ||
|
|
314c3cb516 | ||
|
|
58939bfd8c | ||
|
|
33592b3c0e | ||
|
|
ad9c2549bc | ||
|
|
5152e0b114 | ||
|
|
ca48663efd | ||
|
|
b520b4c37a | ||
|
|
90f07295b1 | ||
|
|
3895c18466 | ||
|
|
2b6a9fc5bb | ||
|
|
052f0b8709 | ||
|
|
a5bb9d962d | ||
|
|
2d9d28aa0f | ||
|
|
69c053a94f | ||
|
|
aaaf1f660c | ||
|
|
8538d83520 | ||
|
|
132c98b767 | ||
|
|
42cac81953 | ||
|
|
3ee4bd65c6 | ||
|
|
fcc7e80bf9 | ||
|
|
e0d43a4c1e | ||
|
|
4966d6c920 | ||
|
|
1fd506f25d | ||
|
|
a99698d1a9 | ||
|
|
774b86c7dc | ||
|
|
2c3e8533c7 | ||
|
|
3eda8af671 | ||
|
|
aa61874848 | ||
|
|
2deab31187 | ||
|
|
b177a56fa2 | ||
|
|
6f0f75cf27 | ||
|
|
39bb2eb9b0 | ||
|
|
7b36bb025a | ||
|
|
aa9a1b7af2 | ||
|
|
caf3552d6b | ||
|
|
6e9897f7fc | ||
|
|
fa9258761e | ||
|
|
003e948899 | ||
|
|
9cd998f219 | ||
|
|
d75b894d9a | ||
|
|
5a3d3b76a7 | ||
|
|
5bc2c207db | ||
|
|
612cf25878 | ||
|
|
5ae4912b45 | ||
|
|
a9a70fd2e9 | ||
|
|
f90856808b | ||
|
|
7dbcaa83bc | ||
|
|
38f10b6e3e | ||
|
|
9c8fa06ce1 | ||
|
|
4efe04774c | ||
|
|
5d60534dc9 | ||
|
|
ac141a4316 | ||
|
|
d22064c6f4 | ||
|
|
189721ebba | ||
|
|
7aa9c63dba | ||
|
|
16e894e300 | ||
|
|
d466705ec0 | ||
|
|
0e97d863ce | ||
|
|
5de64d2ae8 | ||
|
|
ced0398e49 | ||
|
|
d4b57924a7 | ||
|
|
d2def2bea3 | ||
|
|
e65bb84f9f | ||
|
|
69b0aa6118 | ||
|
|
10dc315f3b | ||
|
|
0f1457b5d7 | ||
|
|
090873d4c2 | ||
|
|
da00c168ae | ||
|
|
8286d5a06e | ||
|
|
dc5fb978a7 | ||
|
|
a4ab0cbe09 | ||
|
|
4a341b381e | ||
|
|
3fa98bc1aa | ||
|
|
87e2e87ce6 | ||
|
|
c25b6dc16c | ||
|
|
6a786aa090 | ||
|
|
a97d87bce8 | ||
|
|
fba91329f1 | ||
|
|
353cc3b00f | ||
|
|
057ef63586 | ||
|
|
c9fb38981e | ||
|
|
9ea7de8b44 | ||
|
|
020c8ccd2a | ||
|
|
4d09abe725 | ||
|
|
5079e30caf | ||
|
|
e51f6597ed | ||
|
|
e80a65a3cd | ||
|
|
3e5c1a278b | ||
|
|
4ded60874f | ||
|
|
7c3675c9e1 | ||
|
|
ff1a843de6 | ||
|
|
e6cefcf948 | ||
|
|
a857412f13 | ||
|
|
e507f95b2a | ||
|
|
61cf92c67a | ||
|
|
7a4eddc592 | ||
|
|
3fcbf15915 | ||
|
|
efafb1c28a | ||
|
|
67bedf8648 | ||
|
|
c1d35b0f91 | ||
|
|
d3a715bd6b | ||
|
|
28b52cd24f | ||
|
|
46e77f805c | ||
|
|
dd23db0ad8 | ||
|
|
30cf7f8afe | ||
|
|
7802e0bb88 | ||
|
|
07e75b8550 | ||
|
|
1e9fad8278 | ||
|
|
0975826457 | ||
|
|
e5ff320591 | ||
|
|
02c1e47749 | ||
|
|
a50824eeee | ||
|
|
0c3f9f4ed9 | ||
|
|
6b12601c6f | ||
|
|
225bece44e | ||
|
|
a5a5e73196 | ||
|
|
f8085ed78f | ||
|
|
4e6e84c637 | ||
|
|
846212798c | ||
|
|
571f95eb2f | ||
|
|
202eeea33f | ||
|
|
af212057db | ||
|
|
0fd0fea7e7 | ||
|
|
eb6ef3c8ff | ||
|
|
cdb8d35cf6 | ||
|
|
684dcdcef3 | ||
|
|
155351f5dd | ||
|
|
0419e06e7c | ||
|
|
2ea38bd9a4 | ||
|
|
30db9c30c8 | ||
|
|
a8ef594dab | ||
|
|
9858d5b495 | ||
|
|
20bd85b676 | ||
|
|
83ec8ca24f | ||
|
|
9622d02230 | ||
|
|
b72d0ed37e | ||
|
|
f966e504a5 | ||
|
|
a0366e794b | ||
|
|
3c3ce24397 | ||
|
|
30290f3eb2 | ||
|
|
616431e04b | ||
|
|
d0aeb90f0a | ||
|
|
86220fa721 | ||
|
|
0f58a56a07 | ||
|
|
29451562e3 | ||
|
|
911687af2a | ||
|
|
1c9c33b87b | ||
|
|
f485462af1 | ||
|
|
39d7ceb017 | ||
|
|
8251792a0d | ||
|
|
54ac450f92 | ||
|
|
299e4a497f | ||
|
|
3f851c1fd6 | ||
|
|
8cf16f1049 | ||
|
|
b373aa6250 | ||
|
|
2f4b8cd642 | ||
|
|
85b6df3738 | ||
|
|
c675421a6a | ||
|
|
a7b571e5d0 | ||
|
|
b3a9b7ef0e | ||
|
|
e984893853 | ||
|
|
3024319027 | ||
|
|
21ba652413 | ||
|
|
e4b8cd92f2 | ||
|
|
1a4a9f6501 | ||
|
|
e950cdaf32 | ||
|
|
f97be2f8f3 | ||
|
|
be0c8f4f16 | ||
|
|
46fd2de315 | ||
|
|
72f0d77865 | ||
|
|
d43679d59e | ||
|
|
ce46fb5384 | ||
|
|
43b33cb6de | ||
|
|
0344399253 | ||
|
|
3a9b154cb2 | ||
|
|
1074fbacfe | ||
|
|
00ff3ab380 | ||
|
|
db874a011c | ||
|
|
35a2839a2f | ||
|
|
cdb9b9bb87 | ||
|
|
87a3e4d440 | ||
|
|
284bed677e | ||
|
|
667be460e5 | ||
|
|
c49386bb58 | ||
|
|
adb50a9623 | ||
|
|
ac1d2372f4 | ||
|
|
f54690c829 | ||
|
|
053f9c34aa | ||
|
|
e7dd2b4aee | ||
|
|
6f82f9e01b | ||
|
|
d531730dc1 | ||
|
|
00bdf6aaa6 | ||
|
|
97f3514677 | ||
|
|
137facf95a | ||
|
|
8afba3a5c4 | ||
|
|
9c5383dc37 | ||
|
|
260bc9664e | ||
|
|
655ca83356 | ||
|
|
472bf1665c | ||
|
|
db129cc19b | ||
|
|
b735f8a524 | ||
|
|
1e34764588 | ||
|
|
99aaae491c | ||
|
|
f288581c69 | ||
|
|
3c5d50bce9 | ||
|
|
a01f08391b | ||
|
|
51a1399bca | ||
|
|
b735cb96a0 | ||
|
|
d00c25e107 | ||
|
|
8a5e87b116 | ||
|
|
2779d19d5c | ||
|
|
38d4a8b198 | ||
|
|
ccf98c0c22 | ||
|
|
bd0d91d1b4 | ||
|
|
056f3a6ccb | ||
|
|
20a50f8382 | ||
|
|
21284e7795 | ||
|
|
e0ceed5a63 | ||
|
|
958a2ee6ec | ||
|
|
ea264cb15e | ||
|
|
d8f19e631c | ||
|
|
ce5c0ed5ba | ||
|
|
839ca9ecfb | ||
|
|
720bc12c00 | ||
|
|
1ba845fb06 | ||
|
|
e86fa9d24a | ||
|
|
e348a61085 | ||
|
|
2f70366299 | ||
|
|
c466b20558 | ||
|
|
021f8d25a5 | ||
|
|
a19c3a43d8 | ||
|
|
819923e66d | ||
|
|
6c3100e250 | ||
|
|
1065eda47f | ||
|
|
afd676a958 | ||
|
|
12405b66d0 | ||
|
|
ecd0b6fa83 | ||
|
|
3a8587378c | ||
|
|
a05c08ed48 | ||
|
|
469a90787b | ||
|
|
9e5a9b5ced | ||
|
|
f311ba3f7c | ||
|
|
fc68321bd5 | ||
|
|
8a4173d0ad | ||
|
|
90af31cb2f | ||
|
|
61bcd9337b | ||
|
|
7944045893 | ||
|
|
2c3f83c70d | ||
|
|
92ab705ff5 | ||
|
|
2951f0c40c | ||
|
|
8a23bccb70 | ||
|
|
aa6ad01fb9 | ||
|
|
ef325896c5 | ||
|
|
a270c73d7c | ||
|
|
16feda895d | ||
|
|
874a50d142 | ||
|
|
ae32645470 | ||
|
|
d4412fe06f | ||
|
|
b42c05fb56 | ||
|
|
8d0da61bd4 | ||
|
|
8b8be261cd | ||
|
|
2b6ceed897 | ||
|
|
8abca801a2 | ||
|
|
1eae4425d8 | ||
|
|
a6386bd60e | ||
|
|
1460f002ab | ||
|
|
0d6f736c2c | ||
|
|
da88e11d04 | ||
|
|
0a58767563 | ||
|
|
198525f2ce | ||
|
|
3220702034 | ||
|
|
262711a127 | ||
|
|
078438442a | ||
|
|
5ac20d65ac | ||
|
|
5d2f706c32 | ||
|
|
844a59d880 | ||
|
|
82c742f964 | ||
|
|
b73cfd8a60 | ||
|
|
8466ff0c1a | ||
|
|
1e0fc7eb0d | ||
|
|
74b0c3ea57 | ||
|
|
a735275864 | ||
|
|
8a1d46deb4 | ||
|
|
b624aeec45 | ||
|
|
e83b4d7d42 | ||
|
|
da302fce32 | ||
|
|
e09fec56e2 | ||
|
|
568f952573 | ||
|
|
fc1e488391 | ||
|
|
5781c258e0 | ||
|
|
13c542aeda | ||
|
|
02148f68e5 | ||
|
|
3c84002abd | ||
|
|
01aa57e493 | ||
|
|
af09b4214a | ||
|
|
43b6c71205 | ||
|
|
480e1adfbf | ||
|
|
8c04712784 | ||
|
|
0c61ba8f2d | ||
|
|
485f662d75 | ||
|
|
26caeec0c1 | ||
|
|
4a7cb88a3e | ||
|
|
d11696015d | ||
|
|
3b76ca4f9b | ||
|
|
6f3239d514 | ||
|
|
e8f60d39de | ||
|
|
a3bad8aec4 | ||
|
|
644aa26b75 | ||
|
|
ecd9828afc | ||
|
|
7462d61e16 | ||
|
|
b95ef7250d | ||
|
|
569dec1b0b | ||
|
|
1bc0270d7b | ||
|
|
ebfeebc40b | ||
|
|
ec80b25087 | ||
|
|
7a2278d7b6 | ||
|
|
28b93b70fa | ||
|
|
0431f45190 | ||
|
|
96ce444061 | ||
|
|
32776e7ca3 | ||
|
|
35d516ad4d | ||
|
|
e6a17d27b9 | ||
|
|
52edec1b64 | ||
|
|
6440d2289c | ||
|
|
11afadcea8 | ||
|
|
2e981987f2 | ||
|
|
69efca0bdb | ||
|
|
eefb92367e | ||
|
|
8fa96c2836 | ||
|
|
04dae52a6c | ||
|
|
37c9da1351 | ||
|
|
86d7ec6270 | ||
|
|
82f1e0fe32 | ||
|
|
6ec0aa8894 | ||
|
|
3dbe8bfbbf | ||
|
|
20d82bab78 | ||
|
|
60f6123b64 | ||
|
|
74887a58f6 | ||
|
|
76821d16fc | ||
|
|
233a6b5e90 | ||
|
|
a1d9337c81 | ||
|
|
e3fec6c3e4 | ||
|
|
5de88b4cc1 | ||
|
|
af5e6655e0 | ||
|
|
a8716799c9 | ||
|
|
1672027091 | ||
|
|
934ac886cb | ||
|
|
1b47c4d16f | ||
|
|
e8fd906aa1 | ||
|
|
57b6ea1297 | ||
|
|
102ff15a99 | ||
|
|
19148cfc84 | ||
|
|
97c2bab58a | ||
|
|
1f473228ce | ||
|
|
340a8e4176 | ||
|
|
67201a52cc | ||
|
|
30e164fe0a | ||
|
|
cfcb20a468 | ||
|
|
24eac7ef41 | ||
|
|
94162f1b45 | ||
|
|
01c19efc0c | ||
|
|
e4f2a8a23b | ||
|
|
89b33d2c62 | ||
|
|
3ced35a2f8 | ||
|
|
fd4576b234 | ||
|
|
812ad829c9 | ||
|
|
207a9ba723 | ||
|
|
f9bf17d701 | ||
|
|
43b96ccd1e | ||
|
|
14e8cdd9b2 | ||
|
|
259068b860 | ||
|
|
a1a4192835 | ||
|
|
45f09dcc8c | ||
|
|
15c3f11f4a | ||
|
|
1a21027850 | ||
|
|
8427a9fc68 | ||
|
|
f09b89f975 | ||
|
|
7bec84f767 | ||
|
|
8f4cbcf817 | ||
|
|
c3382274a2 | ||
|
|
493055f861 | ||
|
|
11fbbd49f3 | ||
|
|
8ce37d53cd | ||
|
|
baa4012872 | ||
|
|
16c5eba2be | ||
|
|
ec08cb32aa | ||
|
|
86fb1b938b | ||
|
|
5aa7097a6e | ||
|
|
fd3f520e95 | ||
|
|
49ff92892f | ||
|
|
904f5a2656 | ||
|
|
bb105dd832 | ||
|
|
fd934c0514 | ||
|
|
66609428a2 | ||
|
|
0056e0bc6d | ||
|
|
056b66a764 | ||
|
|
3438a5a374 | ||
|
|
9f3806dabf | ||
|
|
1d4d5cc4e7 | ||
|
|
3901543697 | ||
|
|
a4d73e2a67 | ||
|
|
f6854f58ff | ||
|
|
ad7f7d2890 | ||
|
|
498db63cac | ||
|
|
05659820d0 | ||
|
|
02779ef725 | ||
|
|
935c9b6a42 | ||
|
|
522f7644a3 | ||
|
|
b1a67d1fc5 | ||
|
|
0674c0aff6 | ||
|
|
c8664d5952 | ||
|
|
8ce1e2c956 | ||
|
|
90cca28e01 | ||
|
|
e489b7101c | ||
|
|
eee64c8064 | ||
|
|
a0d4e8dafc | ||
|
|
e6b94df06d | ||
|
|
6f1c7d6253 | ||
|
|
2d5a19b676 | ||
|
|
d1e8dded68 | ||
|
|
9888f66c84 | ||
|
|
5ec51d0ccc | ||
|
|
79f9331073 | ||
|
|
43bcf4ab98 | ||
|
|
f4eae72c48 | ||
|
|
9d14d3e5b7 | ||
|
|
26d3141489 | ||
|
|
5c44f51d6d | ||
|
|
50f3f32ba8 | ||
|
|
8fa858ca8c | ||
|
|
8b1d1671f7 | ||
|
|
73aa35ea2c | ||
|
|
a391445e5f | ||
|
|
530c5d416a | ||
|
|
319a13c0ef | ||
|
|
ec2fedd797 | ||
|
|
7489d19784 | ||
|
|
3b5a0e8d66 | ||
|
|
29ed63286c | ||
|
|
108da4b6d4 | ||
|
|
a00a1c64d1 | ||
|
|
275825cfe1 | ||
|
|
cd2fce56a6 | ||
|
|
cab850fb35 | ||
|
|
045dc600b0 | ||
|
|
1b199c1682 | ||
|
|
936fa637ec | ||
|
|
b9ccb7a892 | ||
|
|
95b33be3cf | ||
|
|
cde27b89c2 | ||
|
|
476e52e004 | ||
|
|
8dc88fe64b | ||
|
|
d00da790af | ||
|
|
c61fac75e4 | ||
|
|
e949af6fd5 | ||
|
|
c1e38b917e | ||
|
|
1244121216 | ||
|
|
46543daa13 | ||
|
|
6277384bf9 | ||
|
|
17549fed9c | ||
|
|
377e42affc | ||
|
|
0ac5d56865 | ||
|
|
b59ee6ad7e | ||
|
|
2d7c5a827f | ||
|
|
85c32ef843 | ||
|
|
dc089d7db9 | ||
|
|
d570f910f8 | ||
|
|
8f2731911b | ||
|
|
9d22420027 | ||
|
|
029da3791b | ||
|
|
caa51b1029 | ||
|
|
2973a03d40 | ||
|
|
679d464f4c | ||
|
|
93c0787efd | ||
|
|
a1708a1469 | ||
|
|
862bf78e63 | ||
|
|
8c01df50a2 | ||
|
|
921932920d | ||
|
|
d021c9b4ae | ||
|
|
309a18e9c0 | ||
|
|
73322c96a6 | ||
|
|
121e7db330 | ||
|
|
9f5e1b59fb | ||
|
|
3282ed4fea | ||
|
|
c7d9192f18 | ||
|
|
bd0f707aed | ||
|
|
8b30634ebe | ||
|
|
ca8acb14f1 | ||
|
|
26b8f8d0d5 | ||
|
|
a066153556 | ||
|
|
2ee7131c28 | ||
|
|
cb1f65732e | ||
|
|
37237d9c10 | ||
|
|
92cc41dec6 | ||
|
|
246dc663c4 | ||
|
|
4ed3235590 | ||
|
|
0939e405b4 | ||
|
|
584c51ef56 | ||
|
|
26ed05ba3a | ||
|
|
018cb91526 | ||
|
|
e16db2233f | ||
|
|
5d90a08011 | ||
|
|
2d3849a671 | ||
|
|
e0dcdc2110 | ||
|
|
22fcee2a16 | ||
|
|
8c0141367b | ||
|
|
5bb72cfed8 | ||
|
|
6d829fa575 | ||
|
|
85ed1b85ae | ||
|
|
c9d0bd2d7f | ||
|
|
ea31d34649 | ||
|
|
1d2f929d3f | ||
|
|
ffbf0804d9 | ||
|
|
ca0b89ecd3 | ||
|
|
dc9d6f6b79 | ||
|
|
f73520559e | ||
|
|
a4df38d963 | ||
|
|
4a162543f6 | ||
|
|
7a3ea37798 | ||
|
|
fdd389d6b7 | ||
|
|
b91fccc0e3 | ||
|
|
d911b075ab | ||
|
|
4339cdd8a4 | ||
|
|
dd32d3a492 | ||
|
|
d5caadd906 | ||
|
|
c4bc3e2687 | ||
|
|
2c5909a138 | ||
|
|
42c13fa584 | ||
|
|
3b442d4bfc | ||
|
|
9831f81b6e | ||
|
|
d3282506c9 | ||
|
|
2afff6c432 | ||
|
|
be3616abe2 | ||
|
|
daa6f5051c | ||
|
|
1e5bd98f02 | ||
|
|
7e5bfa8dd2 | ||
|
|
53363b0618 | ||
|
|
b2f71a2ce1 | ||
|
|
5d4a575919 | ||
|
|
7d521ed3ce | ||
|
|
bb9ad3daa9 | ||
|
|
442f270ee0 | ||
|
|
7ab74c6cc9 | ||
|
|
6d60baa2d6 | ||
|
|
6d3308621f | ||
|
|
fd49be1d9b | ||
|
|
c40a5648dd | ||
|
|
d9a8d2627a | ||
|
|
c7a88e2f12 | ||
|
|
9d2d170c2d | ||
|
|
df3aa22c59 | ||
|
|
a4aabfcdae | ||
|
|
ce99e70bf9 | ||
|
|
963b1aa6b1 | ||
|
|
008ac2876b | ||
|
|
2330b166f6 | ||
|
|
23c0e01565 | ||
|
|
13073bc98d | ||
|
|
8c319903dd | ||
|
|
2334cbd78a | ||
|
|
0cae954f80 | ||
|
|
c60446a015 | ||
|
|
d0c6a4ee6d | ||
|
|
f2d03a511e | ||
|
|
367233c318 | ||
|
|
9461c1692a | ||
|
|
3b32605b5e | ||
|
|
4d21f8d022 | ||
|
|
aac67570d4 | ||
|
|
5f2c465274 | ||
|
|
fdaa0bc876 | ||
|
|
a692d6be09 | ||
|
|
efbb9648c4 | ||
|
|
3d73153e59 | ||
|
|
8fa2256fb0 | ||
|
|
4fe974e7a8 | ||
|
|
21f76a8f27 | ||
|
|
aeb287fa1d | ||
|
|
1405e8821c | ||
|
|
37e31bac5b | ||
|
|
d31b696846 | ||
|
|
cc01c1f0db | ||
|
|
4a7076e01c | ||
|
|
d306bb25dc | ||
|
|
457c80fe76 | ||
|
|
bb972f8449 | ||
|
|
e6ef64968b | ||
|
|
6f3b87cfd1 | ||
|
|
f449feb3f8 | ||
|
|
b179c8e2b7 | ||
|
|
6aa0a4a47f | ||
|
|
766140f483 | ||
|
|
52aa8b868a | ||
|
|
8a3a4d6fae | ||
|
|
94f212a411 | ||
|
|
4e1dce70a3 | ||
|
|
bbf48a0dd0 | ||
|
|
a35e8f3315 | ||
|
|
205de7233e | ||
|
|
abb5dc5739 | ||
|
|
3a5a29efc0 | ||
|
|
9d249406e3 | ||
|
|
42d9e7a090 | ||
|
|
c202c0d705 | ||
|
|
8a1f9b7de6 | ||
|
|
18820c383d | ||
|
|
b38b879ee3 | ||
|
|
7fc7d626bc | ||
|
|
c8a0f1d0de | ||
|
|
c8aedc6f29 | ||
|
|
1e7ce8bb5c | ||
|
|
5386ca1592 | ||
|
|
08ed019426 | ||
|
|
55c6a5aa27 | ||
|
|
bd6bbf864a | ||
|
|
703335c2f1 | ||
|
|
c04fa496bf | ||
|
|
b9d19cfcb4 | ||
|
|
d8f093b226 | ||
|
|
63a2d83d26 | ||
|
|
7e42e98cb6 | ||
|
|
93deebe923 | ||
|
|
1eecf4b2c7 | ||
|
|
e7fc4ef1e7 | ||
|
|
3c54d4af15 | ||
|
|
ebfbf0e1f8 | ||
|
|
14aa4036e0 | ||
|
|
0c60d54c3f | ||
|
|
e510d279a2 | ||
|
|
be6f1f9c4a | ||
|
|
b1fb177e4b | ||
|
|
ec187fe109 | ||
|
|
9ec329b7ae | ||
|
|
d08bd4e866 | ||
|
|
941d5d7cd9 | ||
|
|
ef8d85773c | ||
|
|
e668d488b4 | ||
|
|
d4eb5facfd | ||
|
|
a1285b120b | ||
|
|
8650876e2b | ||
|
|
81ab12c89c | ||
|
|
4ddb8cec85 | ||
|
|
711c566498 | ||
|
|
cc5336900c | ||
|
|
291a8f5c1f | ||
|
|
8c0e98ed43 | ||
|
|
7a976c0239 | ||
|
|
fb557b9130 | ||
|
|
b56a930f60 | ||
|
|
5819ef346b | ||
|
|
b3dcc82c71 | ||
|
|
06e8308dc2 | ||
|
|
c8a9c9b84e | ||
|
|
e85b49c573 | ||
|
|
c7c6dc4e67 | ||
|
|
172d668416 | ||
|
|
b651dc845b | ||
|
|
c755f44153 | ||
|
|
ed28ce1874 | ||
|
|
194af0e985 | ||
|
|
ab3015df6b | ||
|
|
f36df159e0 | ||
|
|
61462cf57e | ||
|
|
427d186c86 | ||
|
|
84e9c47a67 | ||
|
|
18989d593a | ||
|
|
fc624359c5 | ||
|
|
1914573634 | ||
|
|
1fd36458a6 | ||
|
|
37ee0e568f | ||
|
|
3f8363a5b2 | ||
|
|
e6c0011789 | ||
|
|
eb37a495a2 | ||
|
|
54542f7f07 | ||
|
|
8e38910dd8 | ||
|
|
31b3f778fc | ||
|
|
ca3275757b | ||
|
|
501a314597 | ||
|
|
351e0f3a0b |
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 250
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,json}]
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
@@ -1,5 +1 @@
|
||||
vendor/*
|
||||
!/vendor/vendor.js
|
||||
!/modules/default/**
|
||||
!/modules/node_helper
|
||||
!/modules/node_helper/**
|
||||
modules/default/calendar/vendor/*
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
{
|
||||
"rules": {
|
||||
"indent": ["error", "tab"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error"],
|
||||
"max-len": ["error", 250],
|
||||
"curly": "error",
|
||||
"camelcase": ["error", {"properties": "never"}],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": ["error", {"ignoreComments": false }],
|
||||
"no-irregular-whitespace": ["error"]
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc", "jest"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
"es6": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"config": true,
|
||||
"Log": true,
|
||||
"MM": true,
|
||||
"Module": true,
|
||||
"moment": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
@@ -22,5 +20,12 @@
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error"
|
||||
}
|
||||
}
|
||||
|
||||
20
.github/CONTRIBUTING.md
vendored
20
.github/CONTRIBUTING.md
vendored
@@ -1,40 +1,40 @@
|
||||
Contribution Policy for MagicMirror²
|
||||
====================================
|
||||
# Contribution Policy for MagicMirror²
|
||||
|
||||
Thanks for contributing to MagicMirror²!
|
||||
|
||||
We hold our code to standard, and these standards are documented below.
|
||||
|
||||
If you wish to run both linters, use `grunt` without any arguments.
|
||||
If you wish to run our linters, use `npm run lint` without any arguments.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](http://eslint.org) on our JavaScript files.
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
|
||||
Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
|
||||
|
||||
To run ESLint, use `grunt eslint`.
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
|
||||
### CSS: Run StyleLint
|
||||
|
||||
We use [StyleLint](http://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
|
||||
|
||||
To run StyleLint, use `grunt stylelint`.
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
|
||||
### Submitting Issues
|
||||
|
||||
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: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
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, Windows, Mac, Linux, System V UNIX).
|
||||
**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 0.12.13 or later.
|
||||
**Node Version**: Make sure it's version 12 or later (recommended is 14).
|
||||
|
||||
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: MichMich
|
||||
custom: ["https://magicmirror.builders/#donate"]
|
||||
25
.github/ISSUE_TEMPLATE.md
vendored
25
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,28 +1,41 @@
|
||||
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 GitHub repository of the MagicMirror Docker image:
|
||||
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
|
||||
|
||||
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the corresponding repository:
|
||||
|
||||
- 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.
|
||||
|
||||
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, Windows, Mac, Linux, System V UNIX).
|
||||
**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 8 or later.
|
||||
**Node Version**: Make sure it's version 12 or later (recommended is 14).
|
||||
|
||||
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
|
||||
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,25 @@
|
||||
> Please send your pull requests the develop branch.
|
||||
> Don't forget to add the change to CHANGELOG.md.
|
||||
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?
|
||||
> - If so, can you reference the issue like this `Fixes #<issue_number>`?
|
||||
> - What does the pull request accomplish? Use a list if needed.
|
||||
> - If it includes major visual changes please add screenshots.
|
||||
>
|
||||
> 3. Please run `npm run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
> 4. Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
**Note**: Sometimes the development moves very fast. It is highly
|
||||
recommended that you update your branch of `develop` before creating a
|
||||
pull request to send us your changes. This makes everyone's lives
|
||||
easier (including yours) and helps us out on the development team.
|
||||
Thanks!
|
||||
|
||||
|
||||
* Does the pull request solve a **related** issue?
|
||||
* If so, can you reference the issue?
|
||||
* What does the pull request accomplish? Use a list if needed.
|
||||
* If it includes major visual changes please add screenshots.
|
||||
Thanks again and have a nice day!
|
||||
|
||||
6
.github/codecov.yml
vendored
Normal file
6
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
||||
BIN
.github/header.psd
vendored
BIN
.github/header.psd
vendored
Binary file not shown.
37
.github/workflows/automated-tests.yml
vendored
Normal file
37
.github/workflows/automated-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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
Normal file
29
.github/workflows/codecov-test-suites.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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
|
||||
20
.github/workflows/enforce-changelog.yml
vendored
Normal file
20
.github/workflows/enforce-changelog.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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"
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# Various Node ignoramuses.
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -8,15 +7,16 @@ pids
|
||||
*.seed
|
||||
lib-cov
|
||||
coverage
|
||||
.grunt
|
||||
.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/
|
||||
@@ -54,7 +54,6 @@ Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Various Linux ignoramuses.
|
||||
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
@@ -68,6 +67,10 @@ Temporary Items
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
!/config/config.js.sample
|
||||
|
||||
# Vim
|
||||
## swap
|
||||
[._]*.s[a-w][a-z]
|
||||
@@ -77,5 +80,3 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
!/tests/node_modules/**/*
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
/config
|
||||
/coverage
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"font-family-name-quotes": "double-where-recommended",
|
||||
"block-no-empty": false
|
||||
"extends": ["stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
}
|
||||
}
|
||||
|
||||
24
.travis.yml
24
.travis.yml
@@ -1,24 +0,0 @@
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
- lts/*
|
||||
- node
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
before_script:
|
||||
- yarn danger ci
|
||||
- npm install grunt-cli -g
|
||||
- "export DISPLAY=:99.0"
|
||||
- "export ELECTRON_DISABLE_SANDBOX=1"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- sleep 5
|
||||
script:
|
||||
- npm run test:lint
|
||||
- npm run test:e2e
|
||||
- npm run test:unit
|
||||
after_script:
|
||||
- npm list
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
400
CHANGELOG.md
400
CHANGELOG.md
@@ -1,10 +1,308 @@
|
||||
# MagicMirror² Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||
|
||||
## [2.17.1] - 2021-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error when accessing letsencrypt certificates
|
||||
|
||||
## [2.17.0] - 2021-10-01
|
||||
|
||||
Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas.
|
||||
|
||||
### Added
|
||||
|
||||
- Added showTime parameter to clock module for enabling/disabling time display in analog clock.
|
||||
- Added custom electron switches from user config (`config.electronSwitches`).
|
||||
- Added unit tests for updatenotification module.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump electron to v13 (and spectron to v15) and update other dependencies in package.json.
|
||||
- Refactor test configs, use default test config for all tests.
|
||||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Update jsdocs and print warnings during testing too.
|
||||
- Update weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
- Run prettier over all relevant files.
|
||||
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
|
||||
- Update dependencies in package.json.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix undefined error with ignoreToday option in weather module (#2620).
|
||||
- Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632).
|
||||
- Fix black cursor on startup when using electron.
|
||||
- Fix update notification not working for own repository (#2644).
|
||||
|
||||
## [2.16.0] - 2021-07-01
|
||||
|
||||
Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed.
|
||||
|
||||
### Added
|
||||
|
||||
- Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP".
|
||||
- Added German translation for "PRECIP".
|
||||
- Added Dutch translation for "WEEK", "PRECIP", "MODULE_CONFIG_CHANGED" and "MODULE_CONFIG_ERROR".
|
||||
- Added first test for Alert module.
|
||||
- Added support for `dateFormat` when not using `timeFormat: "absolute"`.
|
||||
- Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file.
|
||||
- Added custom-properties for gaps around body and between modules.
|
||||
- Added test case for recurring calendar events.
|
||||
- Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only).
|
||||
- Added list view for newsfeed module.
|
||||
- Added dev dependency jest, switching from mocha to jest.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed).
|
||||
- Use codecov in informational mode.
|
||||
- Refactor code into es6 where possible (e.g. var -> let/const).
|
||||
- Use node v16 in github workflow (replacing node v10).
|
||||
- Moved some files into better suited directories.
|
||||
- Update dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`.
|
||||
- Update dependencies in package.json and migrate husky to v6, fix husky setup in prod environment.
|
||||
- Cleaned up error handling in newsfeed and calendar modules for real.
|
||||
- Updated default WEATHER module such that a provider can optionally set a custom unit-of-measure for precipitation (`weatherObject.precipitationUnits`).
|
||||
- Update documentation.
|
||||
- Update jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests.
|
||||
- Update dependencies in package.json.
|
||||
|
||||
### Removed
|
||||
|
||||
- Switching from mocha to jest so removed following dev dependencies: chai, chai-as-promised, mocha, mocha-each, mocha-logger.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix calendar start function logging inconsistency.
|
||||
- Fix updatenotification start function logging inconsistency.
|
||||
- Checks and applies the showDescription setting for the newsfeed module again.
|
||||
- Fix issue with openweathermap not showing current or forecast info when using onecall API.
|
||||
- Fix tests in weather module and add one for decimalPoint in forecast.
|
||||
- Fix decimalSymbol in the forecast part of the new weather module (#2530).
|
||||
- Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`.
|
||||
- Fix alert not recognizing multiple alerts (#2522).
|
||||
- Fix fetch option httpsAgent to agent in calendar module (#466).
|
||||
- Fix module updatenotification which did not work for repos with many refs (#1907).
|
||||
- Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config").
|
||||
- Fix calendar debug check.
|
||||
- Really run prettier over all files.
|
||||
- Fix logger.js after jest changes, use --forceExit running jest.
|
||||
- Workaround for dev_console test using getWindowCount.
|
||||
|
||||
## [2.15.0] - 2021-04-01
|
||||
|
||||
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Galician language.
|
||||
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||
- Added CodeCov badge to Readme.
|
||||
- Added CURRENTWEATHER_TYPE notification to currentweather and weather module, use it in compliments module.
|
||||
- Added `start:dev` command to the npm scripts for starting electron with devTools open.
|
||||
- Added logging when using deprecated modules weatherforecast or currentweather.
|
||||
- Added Portuguese translations for "MODULE_CONFIG_CHANGED" and "PRECIP".
|
||||
- Respect parameter ColoredSymbolOnly also for custom events.
|
||||
- Added a new parameter to hide time portion on relative times.
|
||||
- `module.show` has now the option for a callback on error.
|
||||
- Added locale to sample config file.
|
||||
- Added support for self-signed certificates for the default calendar module (#466).
|
||||
- Added hiddenOnStartup flag to module config (#2475).
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated markdown files for github.
|
||||
- Cleaned up old code on server side.
|
||||
- Convert `-0` to `0` when displaying temperature.
|
||||
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
|
||||
- Converted newsfeed module to use templates.
|
||||
- Updated documentation and help screen about invalid config files.
|
||||
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Bump electron to v11 and enable contextIsolation.
|
||||
- Don't update the DOM when a module is not displayed.
|
||||
- Cleaned up jsdoc and tests.
|
||||
- Exposed logger as node module for easier access for 3rd party modules.
|
||||
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch`.
|
||||
- Refactored calendar fetcher.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up translations and translator code.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed danger.js library.
|
||||
- Removed `ical` which was substituted by `node-ical` in release `v2.13.0`. Module developers must install this dependency themselves in the module folder if needed.
|
||||
- Removed valid-url library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added default log levels to stop calendar log spamming.
|
||||
- Fix socket.io cors errors, see [breaking change since socket.io v3](https://socket.io/docs/v3/handling-cors/).
|
||||
- Fix Issue with weather forecast icons due to fixed day start and end time (#2221).
|
||||
- Fix empty directory for each module's main javascript file in the inspector.
|
||||
- Fix Issue with weather forecast icons unit tests with different timezones (#2221).
|
||||
- Fix issue with unencoded characters in translated strings when using nunjuck template (`Loading …` as an example).
|
||||
- Fix socket.io backward compatibility with socket v2 clients.
|
||||
- Fix 3rd party module language loading if language is English.
|
||||
- Fix e2e tests after spectron update.
|
||||
- Fix updatenotification creating zombie processes by setting a timeout for the git process.
|
||||
- Fix weather module openweathermap not loading if lat and lon set without onecall.
|
||||
- Fix calendar daylight savings offset calculation if recurring start date before 2007.
|
||||
- Fix calendar time/date adjustment when time with GMT offset is different day (#2488).
|
||||
- Fix calendar daylight savings offset calculation if recurring FULL DAY start date before 2007 (#2483).
|
||||
- Fix newsreaders template, for wrong test for nowrap in 2 places (should be if not).
|
||||
|
||||
## [2.14.0] - 2021-01-01
|
||||
|
||||
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added new log level "debug" to the logger.
|
||||
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||
- Added Chuvash translation.
|
||||
- Added Weatherbit as a provider to Weather module.
|
||||
- Added SMHI as a provider to Weather module.
|
||||
- Added Hindi & Gujarati translation.
|
||||
- Added optional support for DEGREE position in Feels like translation.
|
||||
- Added support for variables in nunjucks templates for translate filter.
|
||||
- Added Chuvash translation.
|
||||
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||
|
||||
### Updated
|
||||
|
||||
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||
- Update dependencies to latest versions.
|
||||
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Update lithuanian translation.
|
||||
- Update config sample.
|
||||
- Highlight required version mismatch.
|
||||
- No select Text for TouchScreen use.
|
||||
- Corrected logic for timeFormat "relative" and "absolute".
|
||||
- Added missing function call in module.show()
|
||||
- Translator variables can have falsy values (e.g. empty string)
|
||||
- Fix issue with weather module with DEGREE label in FEELS like
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed Travis CI integration.
|
||||
|
||||
### Fixed
|
||||
|
||||
- JSON Parse translation files with comments crashing UI. (#2149)
|
||||
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
||||
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
||||
- Fix calendar fetcher subsequent timing. (#2160)
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other than English. (#2221)
|
||||
- Update node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
|
||||
## [2.13.0] - 2020-10-01
|
||||
|
||||
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Added lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
- Added config option to calendar-icons for recurring- and fullday-events.
|
||||
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
|
||||
- Added eslint-plugin for jsdoc comments.
|
||||
- Added new configDeepMerge option for module developers.
|
||||
|
||||
### Updated
|
||||
|
||||
- Change incorrect weather.js default properties.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up jsdoc comments.
|
||||
- Cleaned up clock tests.
|
||||
- Move lodash into devDependencies, update other dependencies.
|
||||
- Switch from ical to node-ical library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix backward compatibility issues for Safari < 11.
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
|
||||
- Fix #2110, #2111, #2118: Recurring full day events should not use timezone adjustment. Just compare month/day.
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
|
||||
Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryanzzhu, @chamakura, @DarthBrento, @Ekristoffe, @khassel, @Legion2, @ndom91, @radokristof, @rejas, @XBCreepinJesus & @ZoneMR.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added option to config the level of logging.
|
||||
- Added prettier for an even cleaner codebase.
|
||||
- Hide Sunrise/Sunset in Weather module.
|
||||
- Hide Sunrise/Sunset in Current Weather module.
|
||||
- Added Met Office DataHub (UK) provider.
|
||||
|
||||
### Updated
|
||||
|
||||
- Cleaned up alert module code.
|
||||
- Cleaned up check_config code.
|
||||
- Replaced grunt-based linters with their non-grunt equivalents.
|
||||
- Switch to most of the eslint:recommended rules and fix warnings.
|
||||
- Replaced insecure links with https ones.
|
||||
- Cleaned up all "no-undef" warnings from eslint.
|
||||
- Added location title wrapping for calendar module.
|
||||
- Updated the BG translation.
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed truetype (ttf) fonts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
🚨 READ THIS BEFORE UPDATING 🚨
|
||||
@@ -14,11 +312,13 @@ In the past years the project has grown a lot. This came with a huge downside: p
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860).
|
||||
|
||||
### Deleted
|
||||
|
||||
- Remove installers.
|
||||
- Remove externalized scripts.
|
||||
- Remove jshint dependency, instead eslint checks your config file now
|
||||
|
||||
### Added
|
||||
|
||||
- Brazilian translation for "FEELS".
|
||||
- Ukrainian translation.
|
||||
- Finnish translation for "PRECIP", "UPDATE_INFO_MULTIPLE" and "UPDATE_INFO_SINGLE".
|
||||
@@ -30,8 +330,10 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Added the ability to configure a list of modules that shouldn't be update checked.
|
||||
- Run linters on git commits
|
||||
- Added date functionality to compliments: display birthday wishes or celebrate an anniversary
|
||||
- Add HTTPS support for clientonly-mode.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Force declaration of public ip address in config file (ISSUE #1852)
|
||||
- Fixes `run-start.sh`: If running in docker-container, don't check the environment, just start electron (ISSUE #1859)
|
||||
- Fix calendar time offset for recurring events crossing Daylight Savings Time (ISSUE #1798)
|
||||
@@ -42,6 +344,7 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Fix update checking skipping 3rd party modules the first time
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove documentation from core repository and link to new dedicated docs site: [docs.magicmirror.builders](https://docs.magicmirror.builders).
|
||||
- Updated config.js.sample: Corrected some grammar on `config.js.sample` comment section.
|
||||
- Removed `run-start.sh` script and update start commands:
|
||||
@@ -51,10 +354,12 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Timestamp in log output now also contains the date
|
||||
- Turkish translation.
|
||||
- Option to configure the size of the currentweather module.
|
||||
- Changed "Gevoelstemperatuur" to "Voelt als" shorter text.
|
||||
|
||||
## [2.10.1] - 2020-01-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated README.md: Added links to the official documentation website and remove links to broken installer.
|
||||
|
||||
## [2.10.0] - 2020-01-01
|
||||
@@ -64,12 +369,14 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Timestamps in log output.
|
||||
- Padding in dateheader mode of the calendar module.
|
||||
- New upgrade script to help users consume regular updates installers/upgrade-script.sh.
|
||||
- New script to help setup pm2, without install installers/fixuppm2.sh.
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated lower bound of `lodash` and `helmet` dependencies for security patches.
|
||||
- Updated compliments.js to handle newline in text, as textfields to not interpolate contents.
|
||||
- Updated raspberry.sh installer script to handle new platform issues, split node/npm, pm2, and screen saver changes.
|
||||
@@ -78,6 +385,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Only check for xwindows running if not on macOS.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue in weatherforecast module where predicted amount of rain was not using the decimal symbol specified in config.js.
|
||||
- Module header now updates correctly, if a module need to dynamically show/hide its header based on a condition.
|
||||
- Fix handling of config.js for serverOnly mode commented out.
|
||||
@@ -90,18 +398,21 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
|
||||
- Spanish translation for "PRECIP".
|
||||
- Adding a Malay (Malaysian) translation for MagicMirror².
|
||||
- Add test check URLs of vendors 200 and 404 HTTP CODE.
|
||||
- Add tests for new weather module and helper to stub ajax requests.
|
||||
|
||||
### Updated
|
||||
|
||||
- Updatenotification module: Display update notification for a limited (configurable) time.
|
||||
- Enabled e2e/vendor_spec.js tests.
|
||||
- The css/custom.css will be rename after the next release. We've add into `run-start.sh` a instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
|
||||
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
|
||||
- Disable sending of notification CLOCK_SECOND when displaySeconds is false.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updatenotification module: Properly handle race conditions, prevent crash.
|
||||
- Send `NEWS_FEED` notification also for the first news messages which are shown.
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
|
||||
@@ -113,6 +424,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
|
||||
- Option to show event location in calendar
|
||||
- Finnish translation for "Feels" and "Weeks"
|
||||
- Russian translation for “Feels”
|
||||
@@ -132,13 +444,16 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
### Updated
|
||||
|
||||
- English translation for "Feels" to "Feels like"
|
||||
- Fixed the example calender url in `config.js.sample`
|
||||
- Fixed the example calendar url in `config.js.sample`
|
||||
- Update `ical.js` to solve various calendar issues.
|
||||
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Update weatherprovider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception, race condition on module update
|
||||
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
|
||||
@@ -150,6 +465,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
|
||||
|
||||
### Updated installer
|
||||
|
||||
- give non-pi2+ users (pi0, odroid, jetson nano, mac, windows, ...) option to continue install
|
||||
- use current username vs hardcoded 'pi' to support non-pi install
|
||||
- check for npm installed. node install doesn't do npm anymore
|
||||
@@ -166,6 +482,7 @@ Fixed `package.json` version number.
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
|
||||
- Italian translation for "Feels"
|
||||
- Basic Klingon (tlhIngan Hol) translations
|
||||
- Disabled the screensaver on raspbian with installation script
|
||||
@@ -179,12 +496,14 @@ Fixed `package.json` version number.
|
||||
- Add `name` config option for calendars to be sent along with event broadcasts
|
||||
|
||||
### Updated
|
||||
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
|
||||
- Updated modernizr code in alert module, fixed a small typo there too
|
||||
- More verbose error message on console if the config is malformed
|
||||
- Updated installer script to install Node.js version 10.x
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
@@ -193,7 +512,7 @@ Fixed `package.json` version number.
|
||||
- Installation script problems with raspbian
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||
@@ -202,6 +521,7 @@ Fixed `package.json` version number.
|
||||
- Fix documentation of `useKMPHwind` option in currentweather
|
||||
|
||||
### New weather module
|
||||
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Dimmed loading indicator for weather forecast.
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
@@ -219,11 +539,13 @@ Fixed `package.json` version number.
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues updating, make sure you are running the latest version of Node.
|
||||
|
||||
### ✨ Experimental ✨
|
||||
|
||||
- New default [module weather](modules/default/weather). This module will eventually replace the current `currentweather` and `weatherforecast` modules. The new module is still pretty experimental, but it's included so you can give it a try and help us improve this module. Please give us you feedback using [this forum post](https://forum.magicmirror.builders/topic/9335/default-weather-module-refactoring).
|
||||
|
||||
A huge, huge, huge thanks to user @fewieden for all his hard work on the new `weather` module!
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to add classes to the cell of symbol, title and time of the events of calendar.
|
||||
- Font-awesome 5, still has 4 for backwards compatibility.
|
||||
- Missing `showEnd` in calendar documentation
|
||||
@@ -238,22 +560,25 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||
|
||||
### Fixed
|
||||
- Allow to parse recurring calendar events where the start date is before 1900
|
||||
|
||||
- Allow parsing recurring calendar events where the start date is before 1900
|
||||
- Fixed Polish translation for Single Update Info
|
||||
- Ignore entries with unparseable details in the calendar module
|
||||
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||
|
||||
### Updated
|
||||
|
||||
- The default calendar setting `showEnd` is changed to `false`.
|
||||
|
||||
### Changed
|
||||
- The Weather Forecast module by default displays the ° symbol after every numeric value to be consistent with the Current Weather module.
|
||||
|
||||
- The Weather Forecast module by default displays the ° symbol after every numeric value to be consistent with the Current Weather module.
|
||||
|
||||
## [2.5.0] - 2018-10-01
|
||||
|
||||
### Added
|
||||
|
||||
- Romanian translation for "Feels"
|
||||
- Support multi-line compliments
|
||||
- Simplified Chinese translation for "Feels"
|
||||
@@ -268,8 +593,9 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Support for showing end of events through config parameters showEnd and dateEndFormat
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed gzip encoded calendar loading issue #1400.
|
||||
- Mixup between german and spanish translation for newsfeed.
|
||||
- Fixed mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
@@ -313,11 +639,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add update translations for Português Brasileiro
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Electron 2.0.0.
|
||||
- Remove yarn-or-npm which breaks production builds.
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
|
||||
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
|
||||
@@ -330,6 +658,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated Italian translation
|
||||
- Updated German translation
|
||||
- Updated Dutch translation
|
||||
@@ -337,6 +666,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
## [2.3.1] - 2018-04-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243)
|
||||
|
||||
## [2.3.0] - 2018-04-01
|
||||
@@ -347,7 +677,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add system notification `MODULE_DOM_CREATED` for notifying each module when their Dom has been fully loaded.
|
||||
- Add types for module.
|
||||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow to scroll in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Changed 'compliments.js' - update DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||
- Automated integration tests translations
|
||||
@@ -357,6 +687,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add dc:date to parsing in newsfeed module, which allows parsing of more rss feeds.
|
||||
|
||||
### Changed
|
||||
|
||||
- Add link to GitHub repository which contains the respective Dockerfile.
|
||||
- Optimized automated unit tests cloneObject, cmpVersions
|
||||
- Update notifications use now translation templates instead of normal strings.
|
||||
@@ -364,6 +695,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Changed Electron dependency to v1.7.13.
|
||||
|
||||
### Fixed
|
||||
|
||||
- News article in fullscreen (iframe) is now shown in front of modules.
|
||||
- Forecast respects maxNumberOfDays regardless of endpoint.
|
||||
- Fix exception on translation of objects.
|
||||
@@ -381,6 +713,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
## [2.2.1] - 2018-01-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed linting errors.
|
||||
|
||||
## [2.2.0] - 2018-01-01
|
||||
@@ -388,10 +721,12 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Changed
|
||||
- Calender week is now handled with a variable translation in order to move number language specific.
|
||||
|
||||
- Calendar week is now handled with a variable translation in order to move number language specific.
|
||||
- Reverted the Electron dependency back to 1.4.15 since newer version don't seem to work on the Raspberry Pi very well.
|
||||
|
||||
### Added
|
||||
|
||||
- Add option to use [Nunjucks](https://mozilla.github.io/nunjucks/) templates in modules. (See `helloworld` module as an example.)
|
||||
- Add Bulgarian translations for MagicMirror² and Alert module.
|
||||
- Add graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting.
|
||||
@@ -405,6 +740,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with calendar module showing more than `maximumEntries` allows
|
||||
- WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP
|
||||
- Correcting translation for Indonesian language
|
||||
@@ -415,9 +751,11 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove Roboto fonts files inside `fonts` and these are installed by npm install command.
|
||||
|
||||
### Added
|
||||
|
||||
- Add `clientonly` script to start only the electron client for a remote server.
|
||||
- Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module.
|
||||
- Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git.
|
||||
@@ -434,6 +772,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add Slack badge to Readme.
|
||||
|
||||
### Updated
|
||||
|
||||
- Changed 'default.js' - listen on all attached interfaces by default.
|
||||
- Add execution of `npm list` after the test are ran in Travis CI.
|
||||
- Change hooks for the vendors e2e tests.
|
||||
@@ -442,6 +781,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Set version of the `express-ipfilter` on 0.3.1.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
@@ -452,11 +792,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
## [2.1.2] - 2017-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
|
||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
|
||||
- Fix the dockerfile to have it running from the first time.
|
||||
|
||||
### Added
|
||||
|
||||
- Add in option to wrap long calendar events to multiple lines using `wrapEvents` configuration option.
|
||||
- Add test e2e `show title newsfeed` for newsfeed module.
|
||||
- Add task to check configuration file.
|
||||
@@ -475,11 +817,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Added Romanian translation.
|
||||
|
||||
### Updated
|
||||
|
||||
- Added missing keys to Polish translation.
|
||||
- Added missing key to German translation.
|
||||
- Added better translation with flexible word order to Finnish translation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix instruction in README for using automatically installer script.
|
||||
- Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
|
||||
- Fix double message about port when server is starting
|
||||
@@ -493,6 +837,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Changed
|
||||
|
||||
- Add `anytime` group for Compliments module.
|
||||
- Compliments module can use remoteFile without default daytime arrays defined.
|
||||
- Installer: Use init config.js from config.js.sample.
|
||||
@@ -508,8 +853,9 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Restructured Test Suite.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)).
|
||||
- Calendar-specific support for `maximumEntries`, and ` maximumNumberOfDays`.
|
||||
- Calendar-specific support for `maximumEntries`, and `maximumNumberOfDays`.
|
||||
- Add loaded function to modules, providing an async callback.
|
||||
- Made default newsfeed module aware of gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Add use pm2 for manager process into Installer RaspberryPi script.
|
||||
@@ -550,11 +896,12 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Added a configurable Week section to the clock module.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update .gitignore to not ignore default modules folder.
|
||||
- Remove white flash on boot up.
|
||||
- Added `update` in Raspberry Pi installation script.
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611))
|
||||
- If units is set to imperial, the showRainAmount option of weatherforecast will show the correct unit.
|
||||
- If units are set to imperial, the showRainAmount option of weatherforecast will show the correct unit.
|
||||
- Module currentWeather: check if temperature received from api is defined.
|
||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||
- Fix newsfeed module bug (removeStartTags)
|
||||
@@ -566,6 +913,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
### Added
|
||||
|
||||
- Finnish translation.
|
||||
- Danish translation.
|
||||
- Turkish translation.
|
||||
@@ -578,7 +926,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Possibility to use the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the ability to set timezone on the date display in the Clock Module
|
||||
@@ -586,7 +934,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Possibility to use currentweather for the compliments
|
||||
- Added option `disabled` for modules.
|
||||
- Added option `address` to set bind address.
|
||||
- Added option `onlyTemp` for currentweather module to show show only current temperature and weather icon.
|
||||
- Added option `onlyTemp` for currentweather module to show only current temperature and weather icon.
|
||||
- Added option `remoteFile` to compliments module to load compliment array from filesystem.
|
||||
- Added option `zoom` to scale the whole mirror display with a given factor.
|
||||
- Added option `roundTemp` for currentweather and weatherforecast modules to display temperatures rounded to nearest integer.
|
||||
@@ -596,6 +944,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add root_path for global vars
|
||||
|
||||
### Updated
|
||||
|
||||
- Modified translations for Frysk.
|
||||
- Modified core English translations.
|
||||
- Updated package.json as a result of Snyk security update.
|
||||
@@ -606,6 +955,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Modules are now secure, and Helmet is now used to prevent abuse of the Mirror's API.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Solve an issue where module margins would appear when the first module of a section was hidden.
|
||||
- Solved visual display errors on chrome, if all modules in one of the right sections are hidden.
|
||||
- Global and Module default config values are no longer modified when setting config values.
|
||||
@@ -616,6 +966,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
## [2.0.5] - 2016-09-20
|
||||
|
||||
### Added
|
||||
|
||||
- Added ability to remove tags from the beginning or end of newsfeed items in 'newsfeed.js'.
|
||||
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
|
||||
- Added CII Badge (we are compliant with the CII Best Practices)
|
||||
@@ -623,11 +974,13 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add the ability to turn off and on the date display in the Clock Module
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix typo in installer.
|
||||
- Add message to unsupported Pi error to mention that Pi Zeros must use server only mode, as ARMv6 is unsupported. Closes #374.
|
||||
- Fix API url for weather API.
|
||||
|
||||
### Updated
|
||||
|
||||
- Force fullscreen when kioskmode is active.
|
||||
- Update the .github templates and information with more modern information.
|
||||
- Update the Gruntfile with a more functional StyleLint implementation.
|
||||
@@ -635,6 +988,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
## [2.0.4] - 2016-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- Brazilian Portuguese Translation.
|
||||
- Option to enable Kiosk mode.
|
||||
- Added ability to start the app with Dev Tools.
|
||||
@@ -642,6 +996,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Greek Translation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||
@@ -650,55 +1005,72 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated package.json to fix possible vulnerabilities. (Using Snyk)
|
||||
- Updated weathericons
|
||||
- Updated default weatherforecast to work with the new icons.
|
||||
- More detailed error message in case config file couldn't be loaded.
|
||||
|
||||
## [2.0.3] - 2016-07-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add max newsitems parameter to the newsfeed module.
|
||||
- Translations for Simplified Chinese, Traditional Chinese and Japanese.
|
||||
- Polish Translation
|
||||
- Add an analog clock in addition to the digital one.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MichMich/MagicMirror/issues/300))
|
||||
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MichMich/MagicMirror/issues/319))
|
||||
|
||||
### Changed
|
||||
|
||||
- Added default string to calendar titleReplace.
|
||||
|
||||
## [2.0.2] - 2016-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Norwegian Translations (nb and nn)
|
||||
- Portuguese Translation
|
||||
- Swedish Translation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added reference to Italian Translation.
|
||||
- Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344)
|
||||
- Added proper User-Agent string to calendar call.
|
||||
|
||||
### Changed
|
||||
|
||||
- Add option to use locationID in weather modules.
|
||||
|
||||
## [2.0.1] - 2016-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- Changelog
|
||||
- Italian Translation
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve the installer by fetching the latest Node.js without any 3rd party interferences.
|
||||
|
||||
## [2.0.0] - 2016-05-03
|
||||
|
||||
### Initial release of MagicMirror²
|
||||
|
||||
It includes (but is not limited to) the following features:
|
||||
|
||||
- Modular system allowing 3rd party plugins.
|
||||
- An Node/Electron based application taking away the need for external servers or browsers.
|
||||
- A complete development API documentation.
|
||||
- Small cute fairies that kiss you while you sleep.
|
||||
|
||||
## [1.0.0] - 2014-02-16
|
||||
|
||||
### Initial release of MagicMirror.
|
||||
This was part of the blogpost: [http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
|
||||
|
||||
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
|
||||
|
||||
106
Gruntfile.js
106
Gruntfile.js
@@ -1,106 +0,0 @@
|
||||
module.exports = function(grunt) {
|
||||
require("time-grunt")(grunt);
|
||||
var fix = (grunt.option("env") || "lint") === "lint";
|
||||
grunt.initConfig({
|
||||
eslint: {
|
||||
options: {
|
||||
fix: fix,
|
||||
configFile: ".eslintrc.json"
|
||||
},
|
||||
target: [
|
||||
"js/*.js",
|
||||
"modules/default/*.js",
|
||||
"modules/default/*/*.js",
|
||||
"serveronly/*.js",
|
||||
"clientonly/*.js",
|
||||
"*.js",
|
||||
"tests/**/*.js",
|
||||
"!modules/default/alert/notificationFx.js",
|
||||
"!modules/default/alert/modernizr.custom.js",
|
||||
"!modules/default/alert/classie.js",
|
||||
"config/*",
|
||||
"translations/translations.js",
|
||||
"vendor/vendor.js"
|
||||
]
|
||||
},
|
||||
stylelint: {
|
||||
simple: {
|
||||
options: {
|
||||
fix: fix,
|
||||
configFile: ".stylelintrc.json"
|
||||
},
|
||||
src: [
|
||||
"css/main.css",
|
||||
"modules/default/calendar/calendar.css",
|
||||
"modules/default/clock/clock_styles.css",
|
||||
"modules/default/currentweather/currentweather.css",
|
||||
"modules/default/weatherforcast/weatherforcast.css"
|
||||
]
|
||||
}
|
||||
},
|
||||
jsonlint: {
|
||||
main: {
|
||||
src: [
|
||||
"package.json",
|
||||
".eslintrc.json",
|
||||
".stylelintrc.json",
|
||||
"translations/*.json",
|
||||
"modules/default/*/translations/*.json",
|
||||
"vendor/package.json"
|
||||
],
|
||||
options: {
|
||||
reporter: "jshint"
|
||||
}
|
||||
}
|
||||
},
|
||||
markdownlint: {
|
||||
all: {
|
||||
options: {
|
||||
config: {
|
||||
"default": true,
|
||||
"line-length": false,
|
||||
"blanks-around-headers": false,
|
||||
"no-duplicate-header": false,
|
||||
"no-inline-html": false,
|
||||
"MD010": false,
|
||||
"MD001": false,
|
||||
"MD031": false,
|
||||
"MD040": false,
|
||||
"MD002": false,
|
||||
"MD029": false,
|
||||
"MD041": false,
|
||||
"MD032": false,
|
||||
"MD036": false,
|
||||
"MD037": false,
|
||||
"MD009": false,
|
||||
"MD018": false,
|
||||
"MD012": false,
|
||||
"MD026": false,
|
||||
"MD038": false,
|
||||
"MD047": false
|
||||
}
|
||||
},
|
||||
src: [
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE.md",
|
||||
"modules/README.md",
|
||||
"modules/default/**/*.md",
|
||||
"!modules/default/calendar/vendor/ical.js/readme.md"
|
||||
]
|
||||
}
|
||||
},
|
||||
yamllint: {
|
||||
all: [
|
||||
".travis.yml"
|
||||
]
|
||||
}
|
||||
});
|
||||
grunt.loadNpmTasks("grunt-eslint");
|
||||
grunt.loadNpmTasks("grunt-stylelint");
|
||||
grunt.loadNpmTasks("grunt-jsonlint");
|
||||
grunt.loadNpmTasks("grunt-yamllint");
|
||||
grunt.loadNpmTasks("grunt-markdownlint");
|
||||
|
||||
grunt.registerTask("default", ["eslint", "stylelint", "jsonlint", "markdownlint", "yamllint"]);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2019 Michael Teeuw
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
||||
29
README.md
29
README.md
@@ -1,33 +1,41 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<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#info=devDependencies"><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"></a>
|
||||
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
||||
<a href="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>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](http://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
**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² focuses on a modular plugin system and uses [Electron](http://electron.atom.io/) as an application wrapper. So no more web server or browser installs necessary!
|
||||
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!
|
||||
|
||||
## Documentation
|
||||
|
||||
For the full documentation including **[installation instructions](https://docs.magicmirror.builders/getting-started/installation.html)**, please visit our dedicated documentation website: [https://docs.magicmirror.builders](https://docs.magicmirror.builders).
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [https://magicmirror.builders](https://magicmirror.builders)
|
||||
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
|
||||
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
|
||||
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
|
||||
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
|
||||
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
|
||||
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards bug reports and documentation. For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards to
|
||||
|
||||
- bug reports
|
||||
- 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)
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
@@ -38,7 +46,6 @@ 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 align="center">
|
||||
<br>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
|
||||
// Use separate scope to prevent global scope pollution
|
||||
(function () {
|
||||
var config = {};
|
||||
const config = {};
|
||||
|
||||
// Helper function to get server address/hostname from either the commandline or env
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
*/
|
||||
function getServerAddress() {
|
||||
// Helper function to get command line parameters
|
||||
// Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
var index = process.argv.indexOf(`--${key}`);
|
||||
var value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
const index = process.argv.indexOf(`--${key}`);
|
||||
const value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
return value !== undefined ? String(value) : defaultValue;
|
||||
}
|
||||
|
||||
@@ -18,37 +26,52 @@
|
||||
["address", "port"].forEach((key) => {
|
||||
config[key] = getCommandLineParameter(key, process.env[key.toUpperCase()]);
|
||||
});
|
||||
|
||||
// determine if "--use-tls"-flag was provided
|
||||
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) {
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on reqested url
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const request = lib.get(url, (response) => {
|
||||
var configData = "";
|
||||
let configData = "";
|
||||
|
||||
// Gather incoming data
|
||||
response.on("data", function(chunk) {
|
||||
response.on("data", function (chunk) {
|
||||
configData += chunk;
|
||||
});
|
||||
// Resolve promise at the end of the HTTP/HTTPS stream
|
||||
response.on("end", function() {
|
||||
response.on("end", function () {
|
||||
resolve(JSON.parse(configData));
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", function(error) {
|
||||
request.on("error", function (error) {
|
||||
reject(new Error(`Unable to read config from server (${url} (${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
console.log(message);
|
||||
} else {
|
||||
console.log("Usage: 'node clientonly --address 192.168.1.10 --port 8080'");
|
||||
console.log("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'");
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
@@ -56,16 +79,18 @@
|
||||
getServerAddress();
|
||||
|
||||
(config.address && config.port) || fail();
|
||||
const prefix = config.tls ? "https://" : "http://";
|
||||
|
||||
// Only start the client if a non-local server was provided
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
|
||||
getServerConfig(`http://${config.address}:${config.port}/config/`)
|
||||
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
|
||||
.then(function (configReturn) {
|
||||
// Pass along the server config via an environment variable
|
||||
var env = Object.create(process.env);
|
||||
var options = { env: env };
|
||||
const env = Object.create(process.env);
|
||||
const options = { env: env };
|
||||
configReturn.address = config.address;
|
||||
configReturn.port = config.port;
|
||||
configReturn.tls = config.tls;
|
||||
env.config = JSON.stringify(configReturn);
|
||||
|
||||
// Spawn electron application
|
||||
@@ -91,7 +116,6 @@
|
||||
console.log(`There something wrong. The clientonly is not running code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
.catch(function (reason) {
|
||||
fail(`Unable to connect to server: (${reason})`);
|
||||
@@ -99,4 +123,4 @@
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}());
|
||||
})();
|
||||
|
||||
2
config/.gitignore
vendored
2
config/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!config.js.sample
|
||||
@@ -1,38 +1,41 @@
|
||||
/* Magic Mirror Config Sample
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* See https://github.com/MichMich/MagicMirror#configuration
|
||||
*
|
||||
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
|
||||
* and https://docs.magicmirror.builders/modules/configuration.html
|
||||
*/
|
||||
|
||||
var config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||
// - another specific IPv4/6 to listen on a specific interface
|
||||
// - "0.0.0.0", "::" to listen on any interface
|
||||
// Default, when address config is left out or empty, is "localhost"
|
||||
let config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||
// - another specific IPv4/6 to listen on a specific interface
|
||||
// - "0.0.0.0", "::" to listen on any interface
|
||||
// Default, when address config is left out or empty, is "localhost"
|
||||
port: 8080,
|
||||
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
|
||||
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
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
|
||||
// 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: [
|
||||
{
|
||||
@@ -54,7 +57,8 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -63,22 +67,26 @@ var config = {
|
||||
position: "lower_third"
|
||||
},
|
||||
{
|
||||
module: "currentweather",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
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"
|
||||
}
|
||||
},
|
||||
{
|
||||
module: "weatherforecast",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
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
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -88,7 +96,7 @@ var config = {
|
||||
feeds: [
|
||||
{
|
||||
title: "New York Times",
|
||||
url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml"
|
||||
url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"
|
||||
}
|
||||
],
|
||||
showSourceTitle: true,
|
||||
@@ -98,7 +106,6 @@ var config = {
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
||||
31
css/custom.css.sample
Normal file
31
css/custom.css.sample
Normal file
@@ -0,0 +1,31 @@
|
||||
/* Magic Mirror Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */
|
||||
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: black;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
117
css/main.css
117
css/main.css
@@ -1,7 +1,29 @@
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: #000;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
||||
html {
|
||||
cursor: none;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
background: var(--color-background);
|
||||
user-select: none;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -9,16 +31,15 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 60px;
|
||||
margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left);
|
||||
position: absolute;
|
||||
height: calc(100% - 120px);
|
||||
width: calc(100% - 120px);
|
||||
background: #000;
|
||||
color: #aaa;
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom));
|
||||
width: calc(100% - var(--gap-body-right) - var(--gap-body-left));
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 2em;
|
||||
line-height: 1.5em;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@@ -27,60 +48,60 @@ body {
|
||||
*/
|
||||
|
||||
.dimmed {
|
||||
color: #666;
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: #fff;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.275;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 30px;
|
||||
line-height: 35px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.225;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 65px;
|
||||
line-height: 65px;
|
||||
font-size: 3.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 75px;
|
||||
line-height: 75px;
|
||||
font-size: 3.75rem;
|
||||
line-height: 1;
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
|
||||
.thin {
|
||||
font-family: Roboto, sans-serif;
|
||||
font-family: var(--font-secondary), sans-serif;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.light {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -94,14 +115,14 @@ body {
|
||||
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
font-size: var(--font-size-small);
|
||||
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #666;
|
||||
border-bottom: 1px solid var(--color-text-dimmed);
|
||||
line-height: 15px;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
sup {
|
||||
@@ -114,11 +135,11 @@ sup {
|
||||
*/
|
||||
|
||||
.module {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: var(--gap-modules);
|
||||
}
|
||||
|
||||
.region.bottom .module {
|
||||
margin-top: 30px;
|
||||
margin-top: var(--gap-modules);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -142,10 +163,10 @@ sup {
|
||||
|
||||
.region.fullscreen {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
left: -60px;
|
||||
right: -60px;
|
||||
bottom: -60px;
|
||||
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));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -162,25 +183,9 @@ sup {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.region.top .container {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.region.bottom .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.region.top .container:empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.region.top.center,
|
||||
.region.bottom.center {
|
||||
left: 50%;
|
||||
-moz-transform: translateX(-50%);
|
||||
-o-transform: translateX(-50%);
|
||||
-webkit-transform: translateX(-50%);
|
||||
-ms-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@@ -194,10 +199,6 @@ sup {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.region.bottom .container:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.region.bottom.right,
|
||||
.region.bottom.center,
|
||||
.region.bottom.left {
|
||||
@@ -213,10 +214,6 @@ sup {
|
||||
.region.middle.center {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
-moz-transform: translateY(-50%);
|
||||
-o-transform: translateY(-50%);
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { danger, fail, warn } from "danger";
|
||||
|
||||
// Check if the CHANGELOG.md file has been edited
|
||||
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.");
|
||||
}
|
||||
|
||||
// Check if the PR request is send to the master branch.
|
||||
// This should only be done by MichMich.
|
||||
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||
// Check if the PR body or title includes the text: #accepted.
|
||||
// If not, the PR will fail.
|
||||
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.");
|
||||
}
|
||||
}
|
||||
34
fonts/package-lock.json
generated
34
fonts/package-lock.json
generated
@@ -1,12 +1,26 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"roboto-fontface": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.8.0.tgz",
|
||||
"integrity": "sha512-ZYzRkETgBrdEGzL5JSKimvjI2CX7ioyZCkX2BpcfyjqI+079W0wHAyj5W4rIZMcDSOHgLZtgz1IdDi/vU77KEQ=="
|
||||
}
|
||||
}
|
||||
"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 +1,15 @@
|
||||
{
|
||||
"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.8.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,94 +2,57 @@
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.ttf") format("truetype");
|
||||
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"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.ttf") format("truetype");
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
roboto-fontface@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/roboto-fontface/-/roboto-fontface-0.8.0.tgz#031a83c8f79932801a57d83bf743f37250163499"
|
||||
100
index.html
100
index.html
@@ -1,55 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
<meta name="google" content="notranslate" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
<meta name="google" content="notranslate" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
<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" />
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
|
||||
<script type="text/javascript">
|
||||
var version = "#VERSION#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="region fullscreen below"><div class="container"></div></div>
|
||||
<div class="region top bar">
|
||||
<div class="container"></div>
|
||||
<div class="region top left"><div class="container"></div></div>
|
||||
<div class="region top center"><div class="container"></div></div>
|
||||
<div class="region top right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region upper third"><div class="container"></div></div>
|
||||
<div class="region middle center"><div class="container"></div></div>
|
||||
<div class="region lower third"><div class="container"><br/></div></div>
|
||||
<div class="region bottom bar">
|
||||
<div class="container"></div>
|
||||
<div class="region bottom left"><div class="container"></div></div>
|
||||
<div class="region bottom center"><div class="container"></div></div>
|
||||
<div class="region bottom right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.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>
|
||||
<script type="text/javascript" src="js/class.js"></script>
|
||||
<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/main.js"></script>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
window.mmVersion = "#VERSION#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="region fullscreen below"><div class="container"></div></div>
|
||||
<div class="region top bar">
|
||||
<div class="container"></div>
|
||||
<div class="region top left"><div class="container"></div></div>
|
||||
<div class="region top center"><div class="container"></div></div>
|
||||
<div class="region top right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region upper third"><div class="container"></div></div>
|
||||
<div class="region middle center"><div class="container"></div></div>
|
||||
<div class="region lower third">
|
||||
<div class="container"><br /></div>
|
||||
</div>
|
||||
<div class="region bottom bar">
|
||||
<div class="container"></div>
|
||||
<div class="region bottom left"><div class="container"></div></div>
|
||||
<div class="region bottom center"><div class="container"></div></div>
|
||||
<div class="region bottom right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.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>
|
||||
<script type="text/javascript" src="js/class.js"></script>
|
||||
<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/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
250
js/app.js
250
js/app.js
@@ -1,27 +1,26 @@
|
||||
/* Magic Mirror
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var fs = require("fs");
|
||||
var Server = require(__dirname + "/server.js");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
|
||||
var path = require("path");
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
|
||||
// Get version number.
|
||||
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
|
||||
console.log("Starting MagicMirror: v" + global.version);
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(__dirname + "/../");
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
@@ -36,107 +35,104 @@ 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) {
|
||||
console.log("Whoops! There was an uncaught exception...");
|
||||
console.error(err);
|
||||
console.log("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
console.log("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
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");
|
||||
});
|
||||
|
||||
/* App - The core app.
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
var App = function() {
|
||||
var nodeHelpers = [];
|
||||
function App() {
|
||||
let nodeHelpers = [];
|
||||
let httpServer;
|
||||
|
||||
/* loadConfig(callback)
|
||||
* Loads the config file. combines it with the defaults,
|
||||
* and runs the callback with the found config as argument.
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* callback with the found config as argument.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* @param {Function} callback Function to be called after loading the config
|
||||
*/
|
||||
var loadConfig = function(callback) {
|
||||
console.log("Loading config ...");
|
||||
var defaults = require(__dirname + "/defaults.js");
|
||||
function loadConfig(callback) {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
var configFilename = path.resolve(global.root_path + "/config/config.js");
|
||||
if (typeof(global.configuration_file) !== "undefined") {
|
||||
configFilename = path.resolve(global.configuration_file);
|
||||
}
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
var c = require(configFilename);
|
||||
const c = require(configFilename);
|
||||
checkDeprecatedOptions(c);
|
||||
var config = Object.assign(defaults, c);
|
||||
const config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
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}`));
|
||||
} else {
|
||||
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var checkDeprecatedOptions = function(userConfig) {
|
||||
var deprecated = require(global.root_path + "/js/deprecated.js");
|
||||
var deprecatedOptions = deprecated.configs;
|
||||
/**
|
||||
* 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) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
var usedDeprecated = [];
|
||||
|
||||
deprecatedOptions.forEach(function(option) {
|
||||
if (userConfig.hasOwnProperty(option)) {
|
||||
usedDeprecated.push(option);
|
||||
}
|
||||
});
|
||||
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
||||
if (usedDeprecated.length > 0) {
|
||||
console.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(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.`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* loadModule(module)
|
||||
/**
|
||||
* Loads a specific module.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModule = function(module, callback) {
|
||||
function loadModule(module, callback) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = __dirname + "/../modules/" + module;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = __dirname + "/../modules/default/" + module;
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
}
|
||||
|
||||
var helperPath = moduleFolder + "/node_helper.js";
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
var loadModule = true;
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
} catch (e) {
|
||||
loadModule = false;
|
||||
console.log("No helper found for module: " + moduleName + ".");
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
if (loadModule) {
|
||||
var Module = require(helperPath);
|
||||
var m = new Module();
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
console.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) {
|
||||
console.log("Version is ok!");
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
console.log("Version is incorrect. Skip module: '" + moduleName + "'");
|
||||
Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -149,45 +145,51 @@ var App = function() {
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* loadModules(modules)
|
||||
/**
|
||||
* Loads all modules.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModules = function(modules, callback) {
|
||||
console.log("Loading module helpers ...");
|
||||
function loadModules(modules, callback) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
var loadNextModule = function() {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function loadNextModule() {
|
||||
if (modules.length > 0) {
|
||||
var nextModule = modules[0];
|
||||
loadModule(nextModule, function() {
|
||||
const nextModule = modules[0];
|
||||
loadModule(nextModule, function () {
|
||||
modules = modules.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
} else {
|
||||
// All modules are loaded
|
||||
console.log("All module helpers loaded.");
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
}
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
var regExStrip0 = /(\.0+)+$/;
|
||||
var segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
var segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
var l = Math.min(segmentsA.length, segmentsB.length);
|
||||
let i, diff;
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
const l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
@@ -198,39 +200,39 @@ var App = function() {
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
/* start(callback)
|
||||
* This methods starts 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.
|
||||
/**
|
||||
* Start the core app.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* 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
|
||||
*/
|
||||
this.start = function(callback) {
|
||||
|
||||
loadConfig(function(c) {
|
||||
this.start = function (callback) {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
|
||||
var modules = [];
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
for (var m in config.modules) {
|
||||
var module = config.modules[m];
|
||||
if (modules.indexOf(module.module) === -1 && !module.disabled) {
|
||||
let modules = [];
|
||||
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function() {
|
||||
var server = new Server(config, function(app, io) {
|
||||
console.log("Server started ...");
|
||||
loadModules(modules, function () {
|
||||
httpServer = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
}
|
||||
|
||||
console.log("Sockets connected & modules started ...");
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
@@ -240,41 +242,49 @@ var App = function() {
|
||||
});
|
||||
};
|
||||
|
||||
/* stop()
|
||||
* This methods stops the core app.
|
||||
* This calls each node_helper's STOP() function, if it exists.
|
||||
/**
|
||||
* Stops the core app. This calls each node_helper's STOP() function, if it
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
*/
|
||||
this.stop = function() {
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
this.stop = function () {
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
}
|
||||
httpServer.close();
|
||||
};
|
||||
|
||||
/* Listen for SIGINT signal and call stop() function.
|
||||
/**
|
||||
* Listen for SIGINT signal and call stop() function.
|
||||
*
|
||||
* Added to fix #1056
|
||||
* Note: this is only used if running `server-only`. Otherwise
|
||||
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
||||
*/
|
||||
process.on("SIGINT", () => {
|
||||
console.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||
Log.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||
/**
|
||||
* Listen to SIGTERM signals so we can stop everything when we
|
||||
* are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = new App();
|
||||
|
||||
73
js/check_config.js
Normal file
73
js/check_config.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/* 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 rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
// 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`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config file using eslint.
|
||||
*/
|
||||
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!");
|
||||
}
|
||||
|
||||
// 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!");
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info(Utils.colors.info("Checking 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
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
75
js/class.js
75
js/class.js
@@ -1,54 +1,65 @@
|
||||
/* global Class, xyz */
|
||||
|
||||
/* Simple JavaScript Inheritance
|
||||
* By John Resig http://ejohn.org/
|
||||
* By John Resig https://johnresig.com/
|
||||
*
|
||||
* Inspired by base2 and Prototype
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
// Inspired by base2 and Prototype
|
||||
(function () {
|
||||
var initializing = false;
|
||||
var fnTest = /xyz/.test(function () { xyz; }) ? /\b_super\b/ : /.*/;
|
||||
let initializing = false;
|
||||
const fnTest = /xyz/.test(function () {
|
||||
xyz;
|
||||
})
|
||||
? /\b_super\b/
|
||||
: /.*/;
|
||||
|
||||
// The base Class implementation (does nothing)
|
||||
this.Class = function () { };
|
||||
this.Class = function () {};
|
||||
|
||||
// Create a new Class that inherits from this class
|
||||
Class.extend = function (prop) {
|
||||
var _super = this.prototype;
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
initializing = true;
|
||||
var prototype = new this();
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
|
||||
// Make a copy of all prototype properties, to prevent reference issues.
|
||||
for (var name in prototype) {
|
||||
prototype[name] = cloneObject(prototype[name]);
|
||||
for (const p in prototype) {
|
||||
prototype[p] = cloneObject(prototype[p]);
|
||||
}
|
||||
|
||||
// Copy the properties over onto the new prototype
|
||||
for (var name in prop) {
|
||||
for (const name in prop) {
|
||||
// Check if we're overwriting an existing function
|
||||
prototype[name] = typeof prop[name] === "function" &&
|
||||
typeof _super[name] === "function" && fnTest.test(prop[name]) ? (function (name, fn) {
|
||||
return function () {
|
||||
var tmp = this._super;
|
||||
prototype[name] =
|
||||
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
? (function (name, fn) {
|
||||
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
|
||||
var 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]) : prop[name];
|
||||
return ret;
|
||||
};
|
||||
})(name, prop[name])
|
||||
: prop[name];
|
||||
}
|
||||
|
||||
// The dummy class constructor
|
||||
/**
|
||||
* The dummy class constructor
|
||||
*/
|
||||
function Class() {
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing && this.init) {
|
||||
@@ -69,15 +80,19 @@
|
||||
};
|
||||
})();
|
||||
|
||||
//Define the clone method for later use.
|
||||
//Helper Method
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
var temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (var key in obj) {
|
||||
const temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (const key in obj) {
|
||||
temp[key] = cloneObject(obj[key]);
|
||||
|
||||
if (key === "lockStrings") {
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
/* exported defaults */
|
||||
/* global mmPort */
|
||||
|
||||
/* Magic Mirror
|
||||
* Config Defauls
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var port = 8080;
|
||||
var address = "localhost";
|
||||
if (typeof(mmPort) !== "undefined") {
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
port = mmPort;
|
||||
}
|
||||
var defaults = {
|
||||
const defaults = {
|
||||
address: address,
|
||||
port: port,
|
||||
basePath: "/",
|
||||
kioskmode: false,
|
||||
electronOptions: {},
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
@@ -42,7 +43,7 @@ var defaults = {
|
||||
module: "helloworld",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
text: "Please create a config file."
|
||||
text: "Please create a config file or check the existing one for errors."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,7 +59,7 @@ var defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already<br>created, your config file probably contains an error.<br>Use a JavaScript linter to validate your file."
|
||||
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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68,14 +69,16 @@ var defaults = {
|
||||
config: {
|
||||
text: "www.michaelteeuw.nl"
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
paths: {
|
||||
modules: "modules",
|
||||
vendor: "vendor"
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = defaults;}
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = defaults;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/* Magic Mirror Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
var deprecated = {
|
||||
configs: ["kioskmode"],
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = deprecated;}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require(__dirname + "/app.js");
|
||||
const core = require("./app.js");
|
||||
const Log = require("logger");
|
||||
|
||||
// Config
|
||||
var config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Module to create native browser window.
|
||||
@@ -14,15 +15,20 @@ const BrowserWindow = electron.BrowserWindow;
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
var electronOptionsDefaults = {
|
||||
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
|
||||
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
zoomFactor: config.zoom
|
||||
},
|
||||
@@ -38,37 +44,55 @@ function createWindow() {
|
||||
electronOptionsDefaults.autoHideMenuBar = true;
|
||||
}
|
||||
|
||||
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
// 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
|
||||
var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`http://${address}:${config.port}`);
|
||||
|
||||
let prefix;
|
||||
if (config["tls"] !== null && config["tls"]) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
}
|
||||
|
||||
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
const devtools = new BrowserWindow(electronOptions);
|
||||
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
}
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// simulate mouse move to hide black cursor on start
|
||||
mainWindow.webContents.on("dom-ready", (event) => {
|
||||
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: 0, y: 0 });
|
||||
});
|
||||
|
||||
// Set responders for window events.
|
||||
mainWindow.on("closed", function() {
|
||||
mainWindow.on("closed", function () {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
if (config.kioskmode) {
|
||||
mainWindow.on("blur", function() {
|
||||
mainWindow.on("blur", function () {
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
mainWindow.on("leave-full-screen", function() {
|
||||
mainWindow.on("leave-full-screen", function () {
|
||||
mainWindow.setFullScreen(true);
|
||||
});
|
||||
|
||||
mainWindow.on("resize", function() {
|
||||
setTimeout(function() {
|
||||
mainWindow.on("resize", function () {
|
||||
setTimeout(function () {
|
||||
mainWindow.reload();
|
||||
}, 1000);
|
||||
});
|
||||
@@ -77,17 +101,22 @@ function createWindow() {
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.on("ready", function() {
|
||||
console.log("Launching application.");
|
||||
app.on("ready", function () {
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function() {
|
||||
createWindow();
|
||||
app.on("window-all-closed", function () {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
app.quit();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", function() {
|
||||
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.
|
||||
if (mainWindow === null) {
|
||||
@@ -102,17 +131,19 @@ app.on("activate", function() {
|
||||
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||
*/
|
||||
app.on("before-quit", (event) => {
|
||||
console.log("Shutting down server...");
|
||||
Log.log("Shutting down server...");
|
||||
event.preventDefault();
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force-quit after 3 seconds.
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force-quit after 3 seconds.
|
||||
core.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
|
||||
core.start(function(c) {
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
core.start(function (c) {
|
||||
config = c;
|
||||
});
|
||||
}
|
||||
|
||||
217
js/loader.js
217
js/loader.js
@@ -1,32 +1,30 @@
|
||||
/* global config, vendor, MM, Log, Module */
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var Loader = (function() {
|
||||
|
||||
const Loader = (function () {
|
||||
/* Create helper variables */
|
||||
|
||||
var loadedModuleFiles = [];
|
||||
var loadedFiles = [];
|
||||
var moduleObjects = [];
|
||||
const loadedModuleFiles = [];
|
||||
const loadedFiles = [];
|
||||
const moduleObjects = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* loadModules()
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
*/
|
||||
var loadModules = function() {
|
||||
const loadModules = function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
var moduleData = getModuleData();
|
||||
|
||||
var loadNextModule = function() {
|
||||
const loadNextModule = function () {
|
||||
if (moduleData.length > 0) {
|
||||
var nextModule = moduleData[0];
|
||||
loadModule(nextModule, function() {
|
||||
const nextModule = moduleData[0];
|
||||
loadModule(nextModule, function () {
|
||||
moduleData = moduleData.slice(1);
|
||||
loadNextModule();
|
||||
});
|
||||
@@ -35,93 +33,100 @@ var Loader = (function() {
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
|
||||
loadFile(config.customCss, function() {
|
||||
loadFile(config.customCss, function () {
|
||||
// custom.css loaded. Start all modules.
|
||||
startModules();
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/* startModules()
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
var startModules = function() {
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
const startModules = function () {
|
||||
for (const module of moduleObjects) {
|
||||
module.start();
|
||||
}
|
||||
|
||||
// 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);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* getAllModules()
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* return array - module data as configured in config
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
var getAllModules = function() {
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
};
|
||||
|
||||
/* getModuleData()
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* return array - Module information.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
var getModuleData = function() {
|
||||
var modules = getAllModules();
|
||||
var moduleFiles = [];
|
||||
const getModuleData = function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
|
||||
for (var m in modules) {
|
||||
var moduleData = modules[m];
|
||||
var module = moduleData.module;
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = config.paths.modules + "/" + module;
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = config.paths.modules + "/" + module;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = config.paths.modules + "/default/" + module;
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
moduleFiles.push({
|
||||
index: m,
|
||||
identifier: "module_" + m + "_" + module,
|
||||
index: index,
|
||||
identifier: "module_" + index + "_" + module,
|
||||
name: moduleName,
|
||||
path: moduleFolder + "/" ,
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
position: moduleData.position,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: (typeof moduleData.classes !== "undefined") ? moduleData.classes + " " + module : module
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return moduleFiles;
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
* Load modules via ajax request and create module objects.
|
||||
/**
|
||||
* Load modules via ajax request and create module objects.s
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* argument module object - Information about the module we want to load.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadModule = function(module, callback) {
|
||||
var url = module.path + "/" + module.file;
|
||||
const loadModule = function (module, callback) {
|
||||
const url = module.path + module.file;
|
||||
|
||||
var afterLoad = function() {
|
||||
var moduleObject = Module.create(module.name);
|
||||
const afterLoad = function () {
|
||||
const moduleObject = Module.create(module.name);
|
||||
if (moduleObject) {
|
||||
bootstrapModule(module, moduleObject, function() {
|
||||
bootstrapModule(module, moduleObject, function () {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
@@ -132,30 +137,30 @@ var Loader = (function() {
|
||||
if (loadedModuleFiles.indexOf(url) !== -1) {
|
||||
afterLoad();
|
||||
} else {
|
||||
loadFile(url, function() {
|
||||
loadFile(url, function () {
|
||||
loadedModuleFiles.push(url);
|
||||
afterLoad();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* bootstrapModule(module, mObj)
|
||||
/**
|
||||
* Bootstrap modules by setting the module data and loading the scripts & styles.
|
||||
*
|
||||
* argument module object - Information about the module we want to load.
|
||||
* argument mObj object - Modules instance.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var bootstrapModule = function(module, mObj, callback) {
|
||||
const bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
|
||||
mObj.setData(module);
|
||||
|
||||
mObj.loadScripts(function() {
|
||||
mObj.loadScripts(function () {
|
||||
Log.log("Scripts loaded for: " + module.name);
|
||||
mObj.loadStyles(function() {
|
||||
mObj.loadStyles(function () {
|
||||
Log.log("Styles loaded for: " + module.name);
|
||||
mObj.loadTranslations(function() {
|
||||
mObj.loadTranslations(function () {
|
||||
Log.log("Translations loaded for: " + module.name);
|
||||
moduleObjects.push(mObj);
|
||||
callback();
|
||||
@@ -164,71 +169,77 @@ var Loader = (function() {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadFile(fileName)
|
||||
/**
|
||||
* Load a script or stylesheet by adding it to the dom.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadFile = function(fileName, callback) {
|
||||
|
||||
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
const loadFile = function (fileName, callback) {
|
||||
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
let script, stylesheet;
|
||||
|
||||
switch (extension.toLowerCase()) {
|
||||
case "js":
|
||||
Log.log("Load script: " + fileName);
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function() {
|
||||
if (typeof callback === "function") {callback();}
|
||||
};
|
||||
script.onerror = function() {
|
||||
console.error("Error on loading script:", fileName);
|
||||
if (typeof callback === "function") {callback();}
|
||||
};
|
||||
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;
|
||||
case "css":
|
||||
Log.log("Load stylesheet: " + fileName);
|
||||
var 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() {
|
||||
console.error("Error on loading stylesheet:", fileName);
|
||||
if (typeof callback === "function") {callback();}
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
break;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/* Public Methods */
|
||||
return {
|
||||
|
||||
/* loadModules()
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function() {
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
},
|
||||
|
||||
/* loadFile()
|
||||
/**
|
||||
* Load a file (script or stylesheet).
|
||||
* Prevent double loading and search for files in the vendor folder.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument module Module Object - the module that calls the loadFile function.
|
||||
* argument callback function - Function called when done.
|
||||
* @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.
|
||||
*/
|
||||
loadFile: function(fileName, module, callback) {
|
||||
|
||||
loadFile: function (fileName, module, callback) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
Log.log("File already loaded: " + fileName);
|
||||
callback();
|
||||
|
||||
94
js/logger.js
94
js/logger.js
@@ -1,27 +1,79 @@
|
||||
/* global console */
|
||||
/* exported Log */
|
||||
|
||||
/* Magic Mirror
|
||||
* Logger
|
||||
* Log
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* 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.
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
});
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
module.exports = factory(root.config);
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
// in nodejs and not running with jest
|
||||
enableLog = process.env.JEST_WORKER_ID === undefined;
|
||||
} else {
|
||||
// in browser and not running with jsdom
|
||||
enableLog = typeof window === "object" && window.name !== "jsdom";
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (enableLog) {
|
||||
logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
warn: Function.prototype.bind.call(console.warn, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
group: Function.prototype.bind.call(console.group, console),
|
||||
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
|
||||
var Log = (function() {
|
||||
return {
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
warn: Function.prototype.bind.call(console.warn, console),
|
||||
group: Function.prototype.bind.call(console.group, console),
|
||||
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
})();
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
logLevel = {
|
||||
debug: function () {},
|
||||
log: function () {},
|
||||
info: function () {},
|
||||
warn: function () {},
|
||||
error: function () {},
|
||||
group: function () {},
|
||||
groupCollapsed: function () {},
|
||||
groupEnd: function () {},
|
||||
time: function () {},
|
||||
timeEnd: function () {},
|
||||
timeStamp: function () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function () {};
|
||||
}
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
||||
421
js/main.js
421
js/main.js
@@ -1,33 +1,30 @@
|
||||
/* global Log, Loader, Module, config, defaults */
|
||||
/* global Loader, defaults, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var MM = (function() {
|
||||
|
||||
var modules = [];
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* createDomObjects()
|
||||
* Create dom objects for all modules that
|
||||
* are configured for a specific position.
|
||||
/**
|
||||
* Create dom objects for all modules that are configured for a specific position.
|
||||
*/
|
||||
var createDomObjects = function() {
|
||||
var domCreationPromises = [];
|
||||
const createDomObjects = function () {
|
||||
const domCreationPromises = [];
|
||||
|
||||
modules.forEach(function(module) {
|
||||
modules.forEach(function (module) {
|
||||
if (typeof module.data.position !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapper = selectWrapper(module.data.position);
|
||||
const wrapper = selectWrapper(module.data.position);
|
||||
|
||||
var dom = document.createElement("div");
|
||||
const dom = document.createElement("div");
|
||||
dom.id = module.identifier;
|
||||
dom.className = module.name;
|
||||
|
||||
@@ -38,104 +35,109 @@ var MM = (function() {
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
var moduleHeader = document.createElement("header");
|
||||
const moduleHeader = document.createElement("header");
|
||||
moduleHeader.innerHTML = module.getHeader();
|
||||
moduleHeader.className = "module-header";
|
||||
dom.appendChild(moduleHeader);
|
||||
|
||||
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
|
||||
moduleHeader.style = "display: none;";
|
||||
moduleHeader.style.display = "none;";
|
||||
} else {
|
||||
moduleHeader.style.display = "block;";
|
||||
}
|
||||
|
||||
var moduleContent = document.createElement("div");
|
||||
const moduleContent = document.createElement("div");
|
||||
moduleContent.className = "module-content";
|
||||
dom.appendChild(moduleContent);
|
||||
|
||||
var domCreationPromise = updateDom(module, 0);
|
||||
const domCreationPromise = updateDom(module, 0);
|
||||
domCreationPromises.push(domCreationPromise);
|
||||
domCreationPromise.then(function() {
|
||||
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
||||
}).catch(Log.error);
|
||||
domCreationPromise
|
||||
.then(function () {
|
||||
sendNotification("MODULE_DOM_CREATED", null, null, module);
|
||||
})
|
||||
.catch(Log.error);
|
||||
});
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
Promise.all(domCreationPromises).then(function() {
|
||||
Promise.all(domCreationPromises).then(function () {
|
||||
sendNotification("DOM_OBJECTS_CREATED");
|
||||
});
|
||||
};
|
||||
|
||||
/* selectWrapper(position)
|
||||
/**
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* argument position string - The name of the position.
|
||||
* @param {string} position The name of the position.
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
*/
|
||||
var selectWrapper = function(position) {
|
||||
var classes = position.replace("_"," ");
|
||||
var parentWrapper = document.getElementsByClassName(classes);
|
||||
const selectWrapper = function (position) {
|
||||
const classes = position.replace("_", " ");
|
||||
const parentWrapper = document.getElementsByClassName(classes);
|
||||
if (parentWrapper.length > 0) {
|
||||
var wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
const wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
if (wrapper.length > 0) {
|
||||
return wrapper[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* argument sendTo Module - The module to send the notification to. (optional)
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||
*/
|
||||
var sendNotification = function(notification, payload, sender, sendTo) {
|
||||
for (var m in modules) {
|
||||
var module = modules[m];
|
||||
const sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (const m in modules) {
|
||||
const module = modules[m];
|
||||
if (module !== sender && (!sendTo || module === sendTo)) {
|
||||
module.notificationReceived(notification, payload, sender);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
*
|
||||
* return Promise - Resolved when the dom is fully updated.
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
var updateDom = function(module, speed) {
|
||||
return new Promise(function(resolve) {
|
||||
var newContentPromise = module.getDom();
|
||||
var newHeader = module.getHeader();
|
||||
const updateDom = function (module, speed) {
|
||||
return new Promise(function (resolve) {
|
||||
const newHeader = module.getHeader();
|
||||
let newContentPromise = module.getDom();
|
||||
|
||||
if (!(newContentPromise instanceof Promise)) {
|
||||
// convert to a promise if not already one to avoid if/else's everywhere
|
||||
newContentPromise = Promise.resolve(newContentPromise);
|
||||
}
|
||||
|
||||
newContentPromise.then(function(newContent) {
|
||||
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
newContentPromise
|
||||
.then(function (newContent) {
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
}).catch(Log.error);
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
})
|
||||
.catch(Log.error);
|
||||
});
|
||||
};
|
||||
|
||||
/* updateDomWithContent(module, speed, newHeader, newContent)
|
||||
/**
|
||||
* Update the dom with the specified content
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
*
|
||||
* return Promise - Resolved when the module dom has been updated.
|
||||
* @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.
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
var updateDomWithContent = function(module, speed, newHeader, newContent) {
|
||||
return new Promise(function(resolve) {
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
return new Promise(function (resolve) {
|
||||
if (module.hidden || !speed) {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
resolve();
|
||||
@@ -153,7 +155,7 @@ var MM = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(module, speed / 2, function() {
|
||||
hideModule(module, speed / 2, function () {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2);
|
||||
@@ -163,66 +165,72 @@ var MM = (function() {
|
||||
});
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Check if the content has changed.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
*
|
||||
* return bool - Does the module need an update?
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
* @returns {boolean} True if the module need an update, false otherwise
|
||||
*/
|
||||
var moduleNeedsUpdate = function(module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
|
||||
var headerNeedsUpdate = false;
|
||||
var contentNeedsUpdate = false;
|
||||
let headerNeedsUpdate = false;
|
||||
let contentNeedsUpdate;
|
||||
|
||||
if (headerWrapper.length > 0) {
|
||||
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
|
||||
}
|
||||
|
||||
var tempContentWrapper = document.createElement("div");
|
||||
const tempContentWrapper = document.createElement("div");
|
||||
tempContentWrapper.appendChild(newContent);
|
||||
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
|
||||
|
||||
return headerNeedsUpdate || contentNeedsUpdate;
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Update the content of a module on screen.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*/
|
||||
var updateModuleContent = function(module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {return;}
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const updateModuleContent = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return;
|
||||
}
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
|
||||
contentWrapper[0].innerHTML = "";
|
||||
contentWrapper[0].appendChild(newContent);
|
||||
|
||||
headerWrapper[0].innerHTML = newHeader;
|
||||
headerWrapper[0].style = headerWrapper.length > 0 && newHeader ? undefined : "display: none;";
|
||||
if (headerWrapper.length > 0 && newHeader) {
|
||||
headerWrapper[0].style.display = "block";
|
||||
} else {
|
||||
headerWrapper[0].style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module to hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
var hideModule = function(module, speed, callback, options) {
|
||||
const hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
// set lockString if set in options.
|
||||
@@ -233,13 +241,13 @@ var MM = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function() {
|
||||
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
|
||||
@@ -248,28 +256,33 @@ var MM = (function() {
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
if (typeof callback === "function") { callback(); }
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") { callback(); }
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module to show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
var showModule = function(module, speed, callback, options) {
|
||||
const showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
// remove lockString if set in options.
|
||||
if (options.lockString) {
|
||||
var index = module.lockStrings.indexOf(options.lockString);
|
||||
if ( index !== -1) {
|
||||
const index = module.lockStrings.indexOf(options.lockString);
|
||||
if (index !== -1) {
|
||||
module.lockStrings.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -278,6 +291,9 @@ var MM = (function() {
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -289,7 +305,7 @@ var MM = (function() {
|
||||
module.lockStrings = [];
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
// Restore the position. See hideModule() for more info.
|
||||
@@ -298,20 +314,24 @@ var MM = (function() {
|
||||
updateWrapperStates();
|
||||
|
||||
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
|
||||
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function() {
|
||||
if (typeof callback === "function") { callback(); }
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// invoke callback
|
||||
if (typeof callback === "function") { callback(); }
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* updateWrapperStates()
|
||||
/**
|
||||
* Checks for all positions if it has visible content.
|
||||
* If not, if will hide the position to prevent unwanted margins.
|
||||
* This method should be called by the show and hide methods.
|
||||
@@ -322,15 +342,15 @@ var MM = (function() {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
var updateWrapperStates = function() {
|
||||
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
const updateWrapperStates = function () {
|
||||
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
positions.forEach(function(position) {
|
||||
var wrapper = selectWrapper(position);
|
||||
var moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
positions.forEach(function (position) {
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
var showWrapper = false;
|
||||
Array.prototype.forEach.call(moduleWrappers, function(moduleWrapper) {
|
||||
let showWrapper = false;
|
||||
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
|
||||
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
||||
showWrapper = true;
|
||||
}
|
||||
@@ -340,10 +360,12 @@ var MM = (function() {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadConfig()
|
||||
* Loads the core config and combines it with de system defaults.
|
||||
/**
|
||||
* Loads the core config and combines it with the system defaults.
|
||||
*/
|
||||
var loadConfig = function() {
|
||||
const loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
/* eslint-disable */
|
||||
if (typeof config === "undefined") {
|
||||
config = defaults;
|
||||
Log.error("Config file is missing! Please create a config file.");
|
||||
@@ -351,56 +373,52 @@ var MM = (function() {
|
||||
}
|
||||
|
||||
config = Object.assign({}, defaults, config);
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
/* setSelectionMethodsForModules()
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* argument modules array - Array of modules.
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
var setSelectionMethodsForModules = function(modules) {
|
||||
|
||||
/* withClass(className)
|
||||
* calls modulesByClass to filter modules with the specified classes.
|
||||
const setSelectionMethodsForModules = function (modules) {
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var withClass = function(className) {
|
||||
const withClass = function (className) {
|
||||
return modulesByClass(className, true);
|
||||
};
|
||||
|
||||
/* exceptWithClass(className)
|
||||
* calls modulesByClass to filter modules without the specified classes.
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptWithClass = function(className) {
|
||||
const exceptWithClass = function (className) {
|
||||
return modulesByClass(className, false);
|
||||
};
|
||||
|
||||
/* modulesByClass(className, include)
|
||||
* filters a collection of modules based on classname(s).
|
||||
/**
|
||||
* Filters a collection of modules based on classname(s).
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* argument include boolean - if the filter should include or exclude the modules with the specific classes.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var modulesByClass = function(className, include) {
|
||||
var searchClasses = className;
|
||||
const modulesByClass = function (className, include) {
|
||||
let searchClasses = className;
|
||||
if (typeof className === "string") {
|
||||
searchClasses = className.split(" ");
|
||||
}
|
||||
|
||||
var newModules = modules.filter(function(module) {
|
||||
var classes = module.data.classes.toLowerCase().split(" ");
|
||||
const newModules = modules.filter(function (module) {
|
||||
const classes = module.data.classes.toLowerCase().split(" ");
|
||||
|
||||
for (var c in searchClasses) {
|
||||
var searchClass = searchClasses[c];
|
||||
for (const searchClass of searchClasses) {
|
||||
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
|
||||
return include;
|
||||
}
|
||||
@@ -413,15 +431,14 @@ var MM = (function() {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* exceptModule(module)
|
||||
/**
|
||||
* Removes a module instance from the collection.
|
||||
*
|
||||
* argument module Module object - The module instance to remove from the collection.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @param {object} module The module instance to remove from the collection.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptModule = function(module) {
|
||||
var newModules = modules.filter(function(mod) {
|
||||
const exceptModule = function (module) {
|
||||
const newModules = modules.filter(function (mod) {
|
||||
return mod.identifier !== module.identifier;
|
||||
});
|
||||
|
||||
@@ -429,47 +446,55 @@ var MM = (function() {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* enumerate(callback)
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
*
|
||||
* argument callback function - The function to execute with the module as an argument.
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
var enumerate = function(callback) {
|
||||
modules.map(function(module) {
|
||||
const enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
callback(module);
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof modules.withClass === "undefined") { Object.defineProperty(modules, "withClass", {value: withClass, enumerable: false}); }
|
||||
if (typeof modules.exceptWithClass === "undefined") { Object.defineProperty(modules, "exceptWithClass", {value: exceptWithClass, enumerable: false}); }
|
||||
if (typeof modules.exceptModule === "undefined") { Object.defineProperty(modules, "exceptModule", {value: exceptModule, enumerable: false}); }
|
||||
if (typeof modules.enumerate === "undefined") { Object.defineProperty(modules, "enumerate", {value: enumerate, enumerable: false}); }
|
||||
if (typeof modules.withClass === "undefined") {
|
||||
Object.defineProperty(modules, "withClass", { value: withClass, enumerable: false });
|
||||
}
|
||||
if (typeof modules.exceptWithClass === "undefined") {
|
||||
Object.defineProperty(modules, "exceptWithClass", { value: exceptWithClass, enumerable: false });
|
||||
}
|
||||
if (typeof modules.exceptModule === "undefined") {
|
||||
Object.defineProperty(modules, "exceptModule", { value: exceptModule, enumerable: false });
|
||||
}
|
||||
if (typeof modules.enumerate === "undefined") {
|
||||
Object.defineProperty(modules, "enumerate", { value: enumerate, enumerable: false });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/* Public Methods */
|
||||
|
||||
/* init()
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function() {
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
},
|
||||
|
||||
/* modulesStarted(moduleObjects)
|
||||
/**
|
||||
* Gets called when all modules are started.
|
||||
*
|
||||
* argument moduleObjects array<Module> - All module instances.
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
modulesStarted: function(moduleObjects) {
|
||||
modulesStarted: function (moduleObjects) {
|
||||
modules = [];
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
modules[module.data.index] = module;
|
||||
}
|
||||
moduleObjects.forEach((module) => modules.push(module));
|
||||
|
||||
Log.info("All modules started!");
|
||||
sendNotification("ALL_MODULES_STARTED");
|
||||
@@ -477,14 +502,14 @@ var MM = (function() {
|
||||
createDomObjects();
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @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: function (notification, payload, sender) {
|
||||
if (arguments.length < 3) {
|
||||
Log.error("sendNotification: Missing arguments.");
|
||||
return;
|
||||
@@ -504,74 +529,78 @@ var MM = (function() {
|
||||
sendNotification(notification, payload, sender);
|
||||
},
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
*/
|
||||
updateDom: function(module, speed) {
|
||||
updateDom: function (module, speed) {
|
||||
if (!(module instanceof Module)) {
|
||||
Log.error("updateDom: Sender should be a module.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!module.data.position) {
|
||||
Log.warn("module tries to update the DOM without being displayed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
},
|
||||
|
||||
/* getModules(module, speed)
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* return array - A collection of all modules currently active.
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function() {
|
||||
getModules: function () {
|
||||
setSelectionMethodsForModules(modules);
|
||||
return modules;
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @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: function (module, speed, callback, options) {
|
||||
module.hidden = true;
|
||||
hideModule(module, speed, callback, options);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @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: function (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
showModule(module, speed, callback, options);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
// Add polyfill for Object.assign.
|
||||
if (typeof Object.assign !== "function") {
|
||||
(function() {
|
||||
Object.assign = function(target) {
|
||||
(function () {
|
||||
Object.assign = function (target) {
|
||||
"use strict";
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError("Cannot convert undefined or null to object");
|
||||
}
|
||||
var output = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var source = arguments[index];
|
||||
const output = Object(target);
|
||||
for (let index = 1; index < arguments.length; index++) {
|
||||
const source = arguments[index];
|
||||
if (source !== undefined && source !== null) {
|
||||
for (var nextKey in source) {
|
||||
for (const nextKey in source) {
|
||||
if (source.hasOwnProperty(nextKey)) {
|
||||
output[nextKey] = source[nextKey];
|
||||
}
|
||||
|
||||
409
js/module.js
409
js/module.js
@@ -1,15 +1,13 @@
|
||||
/* global Log, Class, Loader, Class , MM */
|
||||
/* exported Module */
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var Module = Class.extend({
|
||||
|
||||
const Module = Class.extend({
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
@@ -32,65 +30,66 @@ var Module = Class.extend({
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/* init()
|
||||
* Is called when the module is instantiated.
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init: function () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/* start()
|
||||
* Is called when the module is started.
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
/* getScripts()
|
||||
/**
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getStyles()
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getTranslations()
|
||||
/**
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> - A map with langKeys and filenames.
|
||||
* return Map<String, String> -
|
||||
*
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* getDom()
|
||||
* This method 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 Magic Mirror core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var div = document.createElement("div");
|
||||
var template = self.getTemplate();
|
||||
var templateData = self.getTemplateData();
|
||||
return new Promise((resolve) => {
|
||||
const div = document.createElement("div");
|
||||
const template = this.getTemplate();
|
||||
const templateData = this.getTemplateData();
|
||||
|
||||
// Check to see if we need to render a template string or a file.
|
||||
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||
// the template is a filename
|
||||
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
Log.error(err);
|
||||
}
|
||||
@@ -101,53 +100,52 @@ var Module = Class.extend({
|
||||
});
|
||||
} else {
|
||||
// the template is a template string.
|
||||
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
|
||||
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
|
||||
|
||||
resolve(div);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* getHeader()
|
||||
* This method generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
/**
|
||||
* 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 needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* return string - The header to display above the header.
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader: function () {
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
/* getTemplate()
|
||||
* This method returns the template for the module which is used by the default getDom implementation.
|
||||
/**
|
||||
* Returns the template for the module which is used by the default getDom implementation.
|
||||
* This method needs to be subclassed if the module wants to use a 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.
|
||||
*
|
||||
* return string - The template string of filename.
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return "<div class=\"normal\">" + this.name + "</div><div class=\"small dimmed\">" + this.identifier + "</div>";
|
||||
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
|
||||
},
|
||||
|
||||
/* getTemplateData()
|
||||
* This method returns the data to be used in the template.
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* return Object
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
/* notificationReceived(notification, payload, sender)
|
||||
* This method is called when a notification arrives.
|
||||
* This method is called by the Magic Mirror core.
|
||||
/**
|
||||
* Called by the Magic Mirror core when a notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @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) {
|
||||
if (sender) {
|
||||
@@ -157,49 +155,48 @@ var Module = Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** nunjucksEnvironment()
|
||||
/**
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
|
||||
* @returns Nunjucks Environment
|
||||
*
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function() {
|
||||
nunjucksEnvironment: function () {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), {async: true}), {
|
||||
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true
|
||||
});
|
||||
this._nunjucksEnvironment.addFilter("translate", function(str) {
|
||||
return self.translate(str);
|
||||
|
||||
this._nunjucksEnvironment.addFilter("translate", (str, variables) => {
|
||||
return nunjucks.runtime.markSafe(this.translate(str, variables));
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
* This method is called when a socket notification arrives.
|
||||
/**
|
||||
* 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: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
},
|
||||
|
||||
/* suspend()
|
||||
* This method is called when a module is hidden.
|
||||
/**
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
},
|
||||
|
||||
/* resume()
|
||||
* This method is called when a module is shown.
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
@@ -209,10 +206,10 @@ var Module = Class.extend({
|
||||
* The methods below don"t need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/* setData(data)
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* argument data object - Module data.
|
||||
* @param {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -220,78 +217,78 @@ var Module = Class.extend({
|
||||
this.identifier = data.identifier;
|
||||
this.hidden = false;
|
||||
|
||||
this.setConfig(data.config);
|
||||
this.setConfig(data.config, data.configDeepMerge);
|
||||
},
|
||||
|
||||
/* setConfig(config)
|
||||
/**
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* argument config object - Module config.
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
setConfig: function (config) {
|
||||
this.config = Object.assign({}, this.defaults, config);
|
||||
setConfig: function (config, deep) {
|
||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||
},
|
||||
|
||||
/* socket()
|
||||
* Returns a socket object. If it doesn't exist, it"s created.
|
||||
/**
|
||||
* 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 () {
|
||||
if (typeof this._socket === "undefined") {
|
||||
this._socket = this._socket = new MMSocket(this.name);
|
||||
this._socket = new MMSocket(this.name);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._socket.setNotificationCallback(function (notification, payload) {
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this._socket.setNotificationCallback((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
|
||||
return this._socket;
|
||||
},
|
||||
|
||||
/* file(file)
|
||||
/**
|
||||
* Retrieve the path to a module file.
|
||||
*
|
||||
* argument file string - Filename.
|
||||
*
|
||||
* return string - File path.
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
return (this.data.path + "/" + file).replace("//", "/");
|
||||
},
|
||||
|
||||
/* loadStyles()
|
||||
/**
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadStyles: function (callback) {
|
||||
this.loadDependencies("getStyles", callback);
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
/**
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadScripts: function (callback) {
|
||||
this.loadDependencies("getScripts", callback);
|
||||
},
|
||||
|
||||
/* loadDependencies(funcName, callback)
|
||||
/**
|
||||
* Helper method to load all dependencies.
|
||||
*
|
||||
* argument funcName string - Function name to call to get scripts or styles.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
var self = this;
|
||||
var dependencies = this[funcName]();
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
var loadNextDependency = function () {
|
||||
const loadNextDependency = () => {
|
||||
if (dependencies.length > 0) {
|
||||
var nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, self, function () {
|
||||
const nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, this, () => {
|
||||
dependencies = dependencies.slice(1);
|
||||
loadNextDependency();
|
||||
});
|
||||
@@ -303,179 +300,237 @@ var Module = Class.extend({
|
||||
loadNextDependency();
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
/**
|
||||
* Load all translations.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadTranslations: function (callback) {
|
||||
var self = this;
|
||||
var translations = this.getTranslations();
|
||||
var lang = config.language.toLowerCase();
|
||||
loadTranslations(callback) {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) { break; }
|
||||
const languages = Object.keys(translations);
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (translations) {
|
||||
var translationFile = translations[lang] || undefined;
|
||||
var translationsFallbackFile = translations[first];
|
||||
|
||||
// If a translation file is set, load it and then also load the fallback translation file.
|
||||
// Otherwise only load the fallback translation file.
|
||||
if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
|
||||
Translator.load(self, translationFile, false, function () {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
});
|
||||
} else {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
}
|
||||
} else {
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const translationFile = translations[language];
|
||||
const translationsFallbackFile = translations[fallbackLanguage];
|
||||
|
||||
if (!translationFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
Translator.load(this, translationFile, false, () => {
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* translate(key, defaultValueOrVariables, defaultValue)
|
||||
/**
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* argument key string - The key of the string to translate
|
||||
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||
* argument defaultValue string - The default value with variables. (Optional)
|
||||
* @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) {
|
||||
if(typeof defaultValueOrVariables === "object") {
|
||||
if (typeof defaultValueOrVariables === "object") {
|
||||
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
|
||||
}
|
||||
return Translator.translate(this, key) || defaultValueOrVariables || "";
|
||||
},
|
||||
|
||||
/* updateDom(speed)
|
||||
/**
|
||||
* Request an (animated) update of the module.
|
||||
*
|
||||
* argument speed Number - The speed of the animation. (Optional)
|
||||
* @param {number} [speed] The speed of the animation.
|
||||
*/
|
||||
updateDom: function (speed) {
|
||||
MM.updateDom(this, speed);
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
sendNotification: function (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
},
|
||||
|
||||
/* 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.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide this module.
|
||||
*
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @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 () { };
|
||||
callback = function () {};
|
||||
}
|
||||
|
||||
callback = callback || function () { };
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.hideModule(self, speed, function () {
|
||||
self.suspend();
|
||||
callback();
|
||||
}, options);
|
||||
MM.hideModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.suspend();
|
||||
callback();
|
||||
},
|
||||
options
|
||||
);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show this module.
|
||||
*
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @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 () { };
|
||||
callback = function () {};
|
||||
}
|
||||
|
||||
callback = callback || function () { };
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.showModule(this, speed, function () {
|
||||
self.resume();
|
||||
callback;
|
||||
}, options);
|
||||
MM.showModule(
|
||||
this,
|
||||
speed,
|
||||
() => {
|
||||
this.resume();
|
||||
callback();
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
* -------
|
||||
* this.config = configMerge({}, this.defaults, this.config)
|
||||
* -------
|
||||
* arg1: initial object
|
||||
* arg2: config model
|
||||
* arg3: config to merge
|
||||
* -------
|
||||
* why using it ?
|
||||
* Object.assign() function don't to all job
|
||||
* it don't merge all thing in deep
|
||||
* -> object in object and array is not merging
|
||||
* -------
|
||||
*
|
||||
* 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) {
|
||||
const stack = Array.prototype.slice.call(arguments, 1);
|
||||
let item, key;
|
||||
|
||||
while (stack.length) {
|
||||
item = stack.shift();
|
||||
for (key in item) {
|
||||
if (item.hasOwnProperty(key)) {
|
||||
if (typeof result[key] === "object" && result[key] && Object.prototype.toString.call(result[key]) !== "[object Array]") {
|
||||
if (typeof item[key] === "object" && item[key] !== null) {
|
||||
result[key] = configMerge({}, result[key], item[key]);
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Module.definitions = {};
|
||||
|
||||
Module.create = function (name) {
|
||||
|
||||
// Make sure module definition is available.
|
||||
if (!Module.definitions[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var moduleDefinition = Module.definitions[name];
|
||||
var clonedDefinition = cloneObject(moduleDefinition);
|
||||
const moduleDefinition = Module.definitions[name];
|
||||
const clonedDefinition = cloneObject(moduleDefinition);
|
||||
|
||||
// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
|
||||
var ModuleClass = Module.extend(clonedDefinition);
|
||||
const ModuleClass = Module.extend(clonedDefinition);
|
||||
|
||||
return new ModuleClass();
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
var regExStrip0 = /(\.0+)+$/;
|
||||
var segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
var segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
var l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + version);
|
||||
if (cmpVersions(version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
|
||||
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.log("Version is incorrect. Skip module: '" + name + "'");
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
window.Module = Module;
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
const l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
/* Magic Mirror
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("logger");
|
||||
const express = require("express");
|
||||
|
||||
var Class = require("./class.js");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
|
||||
NodeHelper = Class.extend({
|
||||
init: function() {
|
||||
console.log("Initializing new module helper ...");
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded: function(callback) {
|
||||
console.log("Module helper loaded: " + this.name);
|
||||
loaded(callback) {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start: function() {
|
||||
console.log("Starting module helper: " + this.name);
|
||||
start() {
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
@@ -29,8 +28,8 @@ NodeHelper = Class.extend({
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop: function() {
|
||||
console.log("Stopping module helper: " + this.name);
|
||||
stop() {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
@@ -39,8 +38,8 @@ NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function(notification, payload) {
|
||||
console.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
socketNotificationReceived(notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
@@ -48,7 +47,7 @@ NodeHelper = Class.extend({
|
||||
*
|
||||
* argument name string - Module name.
|
||||
*/
|
||||
setName: function(name) {
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
@@ -57,7 +56,7 @@ NodeHelper = Class.extend({
|
||||
*
|
||||
* argument path string - Module path.
|
||||
*/
|
||||
setPath: function(path) {
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
@@ -67,7 +66,7 @@ NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function(notification, payload) {
|
||||
sendSocketNotification(notification, payload) {
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
@@ -77,11 +76,10 @@ NodeHelper = Class.extend({
|
||||
*
|
||||
* argument app Express app - The Express app object.
|
||||
*/
|
||||
setExpressApp: function(app) {
|
||||
setExpressApp(app) {
|
||||
this.expressApp = app;
|
||||
|
||||
var publicPath = this.path + "/public";
|
||||
app.use("/" + this.name, express.static(publicPath));
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
@@ -90,35 +88,58 @@ NodeHelper = Class.extend({
|
||||
*
|
||||
* argument io Socket.io - The Socket io object.
|
||||
*/
|
||||
setSocketIO: function(io) {
|
||||
var self = this;
|
||||
self.io = io;
|
||||
setSocketIO(io) {
|
||||
this.io = io;
|
||||
|
||||
console.log("Connecting socket for: " + this.name);
|
||||
var namespace = this.name;
|
||||
io.of(namespace).on("connection", function(socket) {
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
var onevent = socket.onevent;
|
||||
socket.onevent = function(packet) {
|
||||
var args = packet.data || [];
|
||||
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("*", function(notification, payload) {
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
//console.log('received message in namespace: ' + namespace);
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
NodeHelper.create = function(moduleDefinition) {
|
||||
NodeHelper.checkFetchStatus = function (response) {
|
||||
// response.status >= 200 && response.status < 300
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Look at the specified error and return an appropriate error type, that
|
||||
* can be translated to a detailed error message
|
||||
*
|
||||
* @param {Error} error the error from fetching something
|
||||
* @returns {string} the string of the detailed error message in the translations
|
||||
*/
|
||||
NodeHelper.checkFetchError = function (error) {
|
||||
let error_type = "MODULE_ERROR_UNSPECIFIED";
|
||||
if (error.code === "EAI_AGAIN") {
|
||||
error_type = "MODULE_ERROR_NO_CONNECTION";
|
||||
} else if (error.message === "Unauthorized") {
|
||||
error_type = "MODULE_ERROR_UNAUTHORIZED";
|
||||
}
|
||||
return error_type;
|
||||
};
|
||||
|
||||
NodeHelper.create = function (moduleDefinition) {
|
||||
return NodeHelper.extend(moduleDefinition);
|
||||
};
|
||||
|
||||
|
||||
100
js/server.js
100
js/server.js
@@ -1,79 +1,96 @@
|
||||
/* Magic Mirror
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
|
||||
var express = require("express");
|
||||
var app = require("express")();
|
||||
var path = require("path");
|
||||
var ipfilter = require("express-ipfilter").IpFilter;
|
||||
var fs = require("fs");
|
||||
var helmet = require("helmet");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
|
||||
var Server = function(config, callback) {
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @param {Function} callback Function called when done.
|
||||
* @class
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
const serverSockets = new Set();
|
||||
|
||||
var port = config.port;
|
||||
if (process.env.MM_PORT) {
|
||||
port = process.env.MM_PORT;
|
||||
}
|
||||
|
||||
var server = null;
|
||||
if(config.useHttps){
|
||||
var options = {
|
||||
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{
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
var io = require("socket.io")(server);
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
console.log("Starting server on port " + port + " ... ");
|
||||
server.on("connection", (socket) => {
|
||||
serverSockets.add(socket);
|
||||
socket.on("close", () => {
|
||||
serverSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, config.address ? config.address : "localhost");
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
|
||||
server.listen(port, config.address || "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
var result = ipfilter(config.ipWhitelist, {mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false})(req, res, function(err) {
|
||||
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();
|
||||
}
|
||||
console.log(err.message);
|
||||
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());
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
var directory;
|
||||
for (var i in directories) {
|
||||
directory = directories[i];
|
||||
|
||||
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) {
|
||||
app.get("/version", function (req, res) {
|
||||
res.send(global.version);
|
||||
});
|
||||
|
||||
app.get("/config", function(req,res) {
|
||||
app.get("/config", function (req, res) {
|
||||
res.send(config);
|
||||
});
|
||||
|
||||
app.get("/", function(req, res) {
|
||||
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
|
||||
app.get("/", function (req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
configFile = "config/config.js";
|
||||
if (typeof(global.configuration_file) !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
@@ -83,6 +100,13 @@ var Server = function(config, callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
|
||||
this.close = function () {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
||||
35
js/socket.js
35
js/socket.js
@@ -1,35 +0,0 @@
|
||||
/* exported Log */
|
||||
|
||||
/* Magic Mirror
|
||||
* Socket Connection
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var MMSocket = function(moduleName) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
}
|
||||
|
||||
self.moduleName = moduleName;
|
||||
|
||||
self.socket = io("http://localhost:8080");
|
||||
self.socket.on("notification", function(data) {
|
||||
MM.sendNotification(data.notification, data.payload, Socket);
|
||||
});
|
||||
|
||||
return {
|
||||
sendMessage: function(notification, payload, sender) {
|
||||
Log.log("Send socket message: " + notification);
|
||||
self.socket.emit("notification", {
|
||||
notification: notification,
|
||||
sender: sender,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,42 +1,53 @@
|
||||
var MMSocket = function(moduleName) {
|
||||
var self = this;
|
||||
/* 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.");
|
||||
}
|
||||
|
||||
self.moduleName = moduleName;
|
||||
this.moduleName = moduleName;
|
||||
|
||||
// Private Methods
|
||||
self.socket = io("/" + self.moduleName, {
|
||||
path: window.location.pathname + "socket.io"
|
||||
let base = "/";
|
||||
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
|
||||
base = config.basePath;
|
||||
}
|
||||
this.socket = io("/" + this.moduleName, {
|
||||
path: base + "socket.io"
|
||||
});
|
||||
var notificationCallback = function() {};
|
||||
|
||||
var onevent = self.socket.onevent;
|
||||
self.socket.onevent = function(packet) {
|
||||
var args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
let notificationCallback = function () {};
|
||||
|
||||
const onevent = this.socket.onevent;
|
||||
this.socket.onevent = (packet) => {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this.socket, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
onevent.call(this.socket, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
self.socket.on("*", function(notification, payload) {
|
||||
this.socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
notificationCallback(notification, payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Public Methods
|
||||
this.setNotificationCallback = function(callback) {
|
||||
this.setNotificationCallback = (callback) => {
|
||||
notificationCallback = callback;
|
||||
};
|
||||
|
||||
this.sendNotification = function(notification, payload) {
|
||||
this.sendNotification = (notification, payload) => {
|
||||
if (typeof payload === "undefined") {
|
||||
payload = {};
|
||||
}
|
||||
self.socket.emit(notification, payload);
|
||||
this.socket.emit(notification, payload);
|
||||
};
|
||||
};
|
||||
|
||||
211
js/translator.js
211
js/translator.js
@@ -1,143 +1,78 @@
|
||||
/* exported Translator */
|
||||
/* global translations */
|
||||
|
||||
/* Magic Mirror
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner http://github.com/CFenner
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Translator = (function() {
|
||||
|
||||
/* loadJSON(file, callback)
|
||||
const Translator = (function () {
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
callback(JSON.parse(stripComments(xhr.responseText)));
|
||||
// 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");
|
||||
}
|
||||
callback(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
/* loadJSON(str, options)
|
||||
* Remove any commenting from a json file so it can be parsed.
|
||||
*
|
||||
* argument str string - The string that contains json with comments.
|
||||
* argument opts function - Strip options.
|
||||
*
|
||||
* return the stripped string.
|
||||
*/
|
||||
function stripComments(str, opts) {
|
||||
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
|
||||
|
||||
var singleComment = 1;
|
||||
var multiComment = 2;
|
||||
|
||||
function stripWithoutWhitespace() {
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripWithWhitespace(str, start, end) {
|
||||
return str.slice(start, end).replace(/\S/g, " ");
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
var currentChar;
|
||||
var nextChar;
|
||||
var insideString = false;
|
||||
var insideComment = false;
|
||||
var offset = 0;
|
||||
var ret = "";
|
||||
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
|
||||
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
currentChar = str[i];
|
||||
nextChar = str[i + 1];
|
||||
|
||||
if (!insideComment && currentChar === "\"") {
|
||||
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
|
||||
if (!escaped) {
|
||||
insideString = !insideString;
|
||||
}
|
||||
}
|
||||
|
||||
if (insideString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideComment && currentChar + nextChar === "//") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = singleComment;
|
||||
i++;
|
||||
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
continue;
|
||||
} else if (insideComment === singleComment && currentChar === "\n") {
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
} else if (!insideComment && currentChar + nextChar === "/*") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = multiComment;
|
||||
i++;
|
||||
continue;
|
||||
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i + 1);
|
||||
offset = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
|
||||
}
|
||||
|
||||
return {
|
||||
coreTranslations: {},
|
||||
coreTranslationsFallback: {},
|
||||
translations: {},
|
||||
translationsFallback: {},
|
||||
|
||||
/* translate(module, key, variables)
|
||||
/**
|
||||
* Load a translation for a given key for a given module.
|
||||
*
|
||||
* argument module Module - The module to load the translation for.
|
||||
* argument key string - The key of the text to translate.
|
||||
* argument variables - The variables to use within the translation template (optional)
|
||||
* @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) {
|
||||
translate: function (module, key, variables) {
|
||||
variables = variables || {}; //Empty object by default
|
||||
|
||||
// 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."
|
||||
/**
|
||||
* 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) {
|
||||
if(Object.prototype.toString.call(template) !== "[object String]") {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
}
|
||||
if(variables.fallback && !template.match(new RegExp("\{.+\}"))) {
|
||||
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
|
||||
template = variables.fallback;
|
||||
}
|
||||
return template.replace(new RegExp("\{([^\}]+)\}", "g"), function(_unused, varName){
|
||||
return variables[varName] || "{"+varName+"}";
|
||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||
});
|
||||
}
|
||||
|
||||
if(this.translations[module.name] && key in this.translations[module.name]) {
|
||||
if (this.translations[module.name] && key in this.translations[module.name]) {
|
||||
// Log.log("Got translation for " + key + " from module translation: ");
|
||||
return createStringFromTemplate(this.translations[module.name][key], variables);
|
||||
}
|
||||
@@ -160,73 +95,61 @@ var Translator = (function() {
|
||||
return key;
|
||||
},
|
||||
|
||||
/* load(module, file, isFallback, callback)
|
||||
/**
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
* argument module Module - The module to load the translation file for.
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument isFallback boolean - Flag to indicate fallback translations.
|
||||
* argument callback function - Function called when done.
|
||||
* @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: function(module, file, isFallback, callback) {
|
||||
if (!isFallback) {
|
||||
Log.log(module.name + " - Load translation: " + file);
|
||||
} else {
|
||||
Log.log(module.name + " - Load translation fallback: " + file);
|
||||
load(module, file, isFallback, callback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if(!this.translationsFallback[module.name]) {
|
||||
loadJSON(module.file(file), function(json) {
|
||||
if (!isFallback) {
|
||||
self.translations[module.name] = json;
|
||||
} else {
|
||||
self.translationsFallback[module.name] = json;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
loadJSON(module.file(file), (json) => {
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* loadCoreTranslations(lang)
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* argument lang String - The language identifier of the core language.
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function(lang) {
|
||||
var self = this;
|
||||
|
||||
loadCoreTranslations: function (lang) {
|
||||
if (lang in translations) {
|
||||
Log.log("Loading core translation file: " + translations[lang]);
|
||||
loadJSON(translations[lang], function(translations) {
|
||||
self.coreTranslations = translations;
|
||||
loadJSON(translations[lang], (translations) => {
|
||||
this.coreTranslations = translations;
|
||||
});
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
self.loadCoreTranslationsFallback();
|
||||
this.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/* loadCoreTranslationsFallback()
|
||||
/**
|
||||
* Load the core translations fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function() {
|
||||
var self = this;
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {break;}
|
||||
|
||||
loadCoreTranslationsFallback: function () {
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], function(translations) {
|
||||
self.coreTranslationsFallback = translations;
|
||||
loadJSON(translations[first], (translations) => {
|
||||
this.coreTranslationsFallback = translations;
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.Translator = Translator;
|
||||
|
||||
11
js/utils.js
11
js/utils.js
@@ -1,19 +1,16 @@
|
||||
/* exported Utils */
|
||||
/* Magic Mirror
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const colors = require("colors/safe");
|
||||
|
||||
var colors = require("colors/safe");
|
||||
|
||||
var Utils = {
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {module.exports = Utils;}
|
||||
|
||||
@@ -6,8 +6,5 @@
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"modules",
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["modules", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
type ModuleProperties = {
|
||||
defaults?: object,
|
||||
start?(): void,
|
||||
getHeader?(): string,
|
||||
getTemplate?(): string,
|
||||
getTemplateData?(): object,
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void,
|
||||
socketNotificationReceived?(notification: string, payload: any): void,
|
||||
suspend?(): void,
|
||||
resume?(): void,
|
||||
getDom?(): HTMLElement,
|
||||
getStyles?(): string[],
|
||||
[key: string]: any,
|
||||
defaults?: object;
|
||||
start?(): void;
|
||||
getHeader?(): string;
|
||||
getTemplate?(): string;
|
||||
getTemplateData?(): object;
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void;
|
||||
socketNotificationReceived?(notification: string, payload: any): void;
|
||||
suspend?(): void;
|
||||
resume?(): void;
|
||||
getDom?(): HTMLElement;
|
||||
getStyles?(): string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
@@ -18,14 +18,14 @@ export declare const Module: {
|
||||
};
|
||||
|
||||
export declare const Log: {
|
||||
info(message?: any, ...optionalParams: any[]): void,
|
||||
log(message?: any, ...optionalParams: any[]): void,
|
||||
error(message?: any, ...optionalParams: any[]): void,
|
||||
warn(message?: any, ...optionalParams: any[]): void,
|
||||
group(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupEnd(): void,
|
||||
time(timerName?: string): void,
|
||||
timeEnd(timerName?: string): void,
|
||||
timeStamp(timerName?: string): void,
|
||||
};
|
||||
info(message?: any, ...optionalParams: any[]): void;
|
||||
log(message?: any, ...optionalParams: any[]): void;
|
||||
error(message?: any, ...optionalParams: any[]): void;
|
||||
warn(message?: any, ...optionalParams: any[]): void;
|
||||
group(groupTitle?: string, ...optionalParams: any[]): void;
|
||||
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void;
|
||||
groupEnd(): void;
|
||||
time(timerName?: string): void;
|
||||
timeEnd(timerName?: string): void;
|
||||
timeStamp(timerName?: string): void;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Module: Alert
|
||||
|
||||
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,13 +1,12 @@
|
||||
/* global Module */
|
||||
/* global NotificationFx */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll http://paulvincentroll.com
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
Module.register("alert",{
|
||||
Module.register("alert", {
|
||||
defaults: {
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
effect: "slide",
|
||||
@@ -18,31 +17,33 @@ Module.register("alert",{
|
||||
//Position
|
||||
position: "center",
|
||||
//shown at startup
|
||||
welcome_message: false,
|
||||
welcome_message: false
|
||||
},
|
||||
getScripts: function() {
|
||||
return ["classie.js", "modernizr.custom.js", "notificationFx.js"];
|
||||
getScripts: function () {
|
||||
return ["notificationFx.js"];
|
||||
},
|
||||
getStyles: function() {
|
||||
return ["ns-default.css", "font-awesome.css"];
|
||||
getStyles: function () {
|
||||
return ["notificationFx.css", "font-awesome.css"];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function() {
|
||||
getTranslations: function () {
|
||||
return {
|
||||
en: "translations/en.json",
|
||||
de: "translations/de.json",
|
||||
nl: "translations/nl.json",
|
||||
nl: "translations/nl.json"
|
||||
};
|
||||
},
|
||||
show_notification: function(message) {
|
||||
if (this.config.effect === "slide") {this.config.effect = this.config.effect + "-" + this.config.position;}
|
||||
msg = "";
|
||||
show_notification: function (message) {
|
||||
if (this.config.effect === "slide") {
|
||||
this.config.effect = this.config.effect + "-" + this.config.position;
|
||||
}
|
||||
let msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
}
|
||||
if (message.message){
|
||||
if (msg !== ""){
|
||||
msg+= "<br />";
|
||||
if (message.message) {
|
||||
if (msg !== "") {
|
||||
msg += "<br />";
|
||||
}
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
}
|
||||
@@ -54,37 +55,40 @@ Module.register("alert",{
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function(params, sender) {
|
||||
var self = this;
|
||||
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.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;
|
||||
image = "";
|
||||
} 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 />";
|
||||
} 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
|
||||
var overlay = document.createElement("div");
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "overlay";
|
||||
overlay.innerHTML += "<div class=\"black_overlay\"></div>";
|
||||
overlay.innerHTML += '<div class="black_overlay"></div>';
|
||||
document.body.insertBefore(overlay, document.body.firstChild);
|
||||
|
||||
//If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender);
|
||||
this.hide_alert(sender, false);
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
var message = "";
|
||||
let message = "";
|
||||
if (params.title) {
|
||||
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||
}
|
||||
if (params.message) {
|
||||
if (message !== ""){
|
||||
if (message !== "") {
|
||||
message += "<br />";
|
||||
}
|
||||
|
||||
@@ -96,40 +100,49 @@ Module.register("alert",{
|
||||
message: image + message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
setTimeout(function() {
|
||||
self.hide_alert(sender);
|
||||
setTimeout(() => {
|
||||
this.hide_alert(sender);
|
||||
}, params.timer);
|
||||
}
|
||||
|
||||
},
|
||||
hide_alert: function(sender) {
|
||||
hide_alert: function (sender, close = true) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss();
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
var overlay = document.getElementById("overlay");
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
},
|
||||
setPosition: function(pos) {
|
||||
setPosition: function (pos) {
|
||||
//Add css to body depending on the set position for notifications
|
||||
var sheet = document.createElement("style");
|
||||
if (pos === "center") {sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";}
|
||||
if (pos === "right") {sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";}
|
||||
if (pos === "left") {sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";}
|
||||
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) {
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") { payload.type = "alert"; }
|
||||
if (typeof payload.type === "undefined") {
|
||||
payload.type = "alert";
|
||||
}
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type === "notification") {
|
||||
@@ -139,15 +152,14 @@ Module.register("alert",{
|
||||
this.hide_alert(sender);
|
||||
}
|
||||
},
|
||||
start: function() {
|
||||
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});
|
||||
if (this.config.welcome_message === true) {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
|
||||
} else {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
|
||||
}
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*!
|
||||
* classie - class helper functions
|
||||
* from bonzo https://github.com/ded/bonzo
|
||||
*
|
||||
* classie.has( elem, 'my-class' ) -> true/false
|
||||
* classie.add( elem, 'my-new-class' )
|
||||
* classie.remove( elem, 'my-unwanted-class' )
|
||||
* classie.toggle( elem, 'my-class' )
|
||||
*/
|
||||
// jscs:disable
|
||||
/*jshint browser: true, strict: true, undef: true */
|
||||
/*global define: false */
|
||||
|
||||
(function(window) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// class helper functions from bonzo https://github.com/ded/bonzo
|
||||
|
||||
function classReg(className) {
|
||||
return new RegExp("(^|\\s+)" + className + "(\\s+|$)");
|
||||
}
|
||||
|
||||
// classList support for class management
|
||||
// altho to be fair, the api sucks because it won't accept multiple classes at once
|
||||
var hasClass, addClass, removeClass;
|
||||
|
||||
if ("classList" in document.documentElement) {
|
||||
hasClass = function(elem, c) {
|
||||
return elem.classList.contains(c);
|
||||
};
|
||||
addClass = function(elem, c) {
|
||||
elem.classList.add(c);
|
||||
};
|
||||
removeClass = function(elem, c) {
|
||||
elem.classList.remove(c);
|
||||
};
|
||||
} else {
|
||||
hasClass = function(elem, c) {
|
||||
return classReg(c).test(elem.className);
|
||||
};
|
||||
addClass = function(elem, c) {
|
||||
if (!hasClass(elem, c)) {
|
||||
elem.className = elem.className + " " + c;
|
||||
}
|
||||
};
|
||||
removeClass = function(elem, c) {
|
||||
elem.className = elem.className.replace(classReg(c), " ");
|
||||
};
|
||||
}
|
||||
|
||||
function toggleClass(elem, c) {
|
||||
var fn = hasClass(elem, c) ? removeClass : addClass;
|
||||
fn(elem, c);
|
||||
}
|
||||
|
||||
var classie = {
|
||||
// full names
|
||||
hasClass: hasClass,
|
||||
addClass: addClass,
|
||||
removeClass: removeClass,
|
||||
toggleClass: toggleClass,
|
||||
// short names
|
||||
has: hasClass,
|
||||
add: addClass,
|
||||
remove: removeClass,
|
||||
toggle: toggleClass
|
||||
};
|
||||
|
||||
// transport
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// AMD
|
||||
define(classie);
|
||||
} else {
|
||||
// browser global
|
||||
window.classie = classie;
|
||||
}
|
||||
|
||||
})(window);
|
||||
File diff suppressed because one or more lines are too long
932
modules/default/alert/notificationFx.css
Normal file
932
modules/default/alert/notificationFx.css
Normal file
@@ -0,0 +1,932 @@
|
||||
/* Based on work by https://tympanus.net/codrops/licensing/ */
|
||||
|
||||
.ns-box {
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 1;
|
||||
font-size: 70%;
|
||||
position: relative;
|
||||
display: table;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
border-color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.ns-alert {
|
||||
border-style: solid;
|
||||
border-color: var(--color-text-bright);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 3;
|
||||
color: var(--color-text-bright);
|
||||
font-size: 70%;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
right: 0;
|
||||
left: 0;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
top: 40%;
|
||||
width: 40%;
|
||||
height: auto;
|
||||
word-wrap: break-word;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.black_overlay {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[class^="ns-effect-"].ns-growl.ns-hide,
|
||||
[class*=" ns-effect-"].ns-growl.ns-hide {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.ns-effect-flip {
|
||||
transform-origin: 50% 100%;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.ns-effect-flip.ns-show,
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipFront;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.ns-effect-flip.ns-hide {
|
||||
animation-name: animFlipBack;
|
||||
}
|
||||
|
||||
@keyframes animFlipFront {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, -90deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(1000px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFlipBack {
|
||||
0% {
|
||||
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(1000px);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-bouncyflip.ns-show,
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInX;
|
||||
animation-duration: 0.8s;
|
||||
}
|
||||
|
||||
@keyframes flipInX {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 20deg);
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -10deg);
|
||||
transition-timing-function: ease-in;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, 5deg);
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(400px);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-bouncyflip.ns-hide {
|
||||
animation-name: flipInXSimple;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes flipInXSimple {
|
||||
0% {
|
||||
transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(400px);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-exploader {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.ns-effect-exploader p {
|
||||
padding: 0.25em 2em 0.25em 3em;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show {
|
||||
animation-name: animLoad;
|
||||
animation-duration: 1s;
|
||||
}
|
||||
|
||||
@keyframes animLoad {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale3d(0, 0.3, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-box-inner,
|
||||
.ns-effect-exploader.ns-show .ns-close {
|
||||
animation-fill-mode: both;
|
||||
animation-duration: 0.3s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-close {
|
||||
animation-name: animFade;
|
||||
}
|
||||
|
||||
.ns-effect-exploader.ns-show .ns-box-inner {
|
||||
animation-name: animFadeMove;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@keyframes animFadeMove {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 10px, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-scale.ns-show,
|
||||
.ns-effect-scale.ns-hide {
|
||||
animation-name: animScale;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animScale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-show {
|
||||
animation-name: animJelly;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.ns-effect-jelly.ns-hide {
|
||||
animation-name: animFade;
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes animFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animJelly {
|
||||
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);
|
||||
}
|
||||
|
||||
4.166667% {
|
||||
transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 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);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(0.92038, 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);
|
||||
}
|
||||
|
||||
12.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 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);
|
||||
}
|
||||
|
||||
16.666667% {
|
||||
transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 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);
|
||||
}
|
||||
|
||||
20.833333% {
|
||||
transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 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);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1.04487, 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);
|
||||
}
|
||||
|
||||
29.166667% {
|
||||
transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 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);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 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);
|
||||
}
|
||||
|
||||
37.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 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);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(0.99368, 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);
|
||||
}
|
||||
|
||||
45.833333% {
|
||||
transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 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);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 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);
|
||||
}
|
||||
|
||||
54.166667% {
|
||||
transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 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);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(0.99822, 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);
|
||||
}
|
||||
|
||||
62.5% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 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);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 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);
|
||||
}
|
||||
|
||||
70.833333% {
|
||||
transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 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);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: matrix3d(1.001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
77.083333% {
|
||||
transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 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);
|
||||
}
|
||||
|
||||
81.25% {
|
||||
transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 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);
|
||||
}
|
||||
|
||||
85.416667% {
|
||||
transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 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);
|
||||
}
|
||||
|
||||
89.583333% {
|
||||
transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 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);
|
||||
}
|
||||
|
||||
93.75% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 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);
|
||||
}
|
||||
|
||||
97.916667% {
|
||||
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-show {
|
||||
animation-name: animSlideElasticLeft;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticLeft {
|
||||
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);
|
||||
}
|
||||
|
||||
3.333333% {
|
||||
transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1);
|
||||
}
|
||||
|
||||
5% {
|
||||
transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 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);
|
||||
}
|
||||
|
||||
8.333333% {
|
||||
transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 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);
|
||||
}
|
||||
|
||||
13.333333% {
|
||||
transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 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);
|
||||
}
|
||||
|
||||
18.333333% {
|
||||
transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 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);
|
||||
}
|
||||
|
||||
23.333333% {
|
||||
transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 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);
|
||||
}
|
||||
|
||||
28.333333% {
|
||||
transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 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);
|
||||
}
|
||||
|
||||
33.333333% {
|
||||
transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1);
|
||||
}
|
||||
|
||||
36.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1);
|
||||
}
|
||||
|
||||
38.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1);
|
||||
}
|
||||
|
||||
43.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1);
|
||||
}
|
||||
|
||||
46.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1);
|
||||
}
|
||||
|
||||
48.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1);
|
||||
}
|
||||
|
||||
51.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1);
|
||||
}
|
||||
|
||||
53.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1);
|
||||
}
|
||||
|
||||
56.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1);
|
||||
}
|
||||
|
||||
58.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1);
|
||||
}
|
||||
|
||||
61.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1);
|
||||
}
|
||||
|
||||
63.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1);
|
||||
}
|
||||
|
||||
65% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1);
|
||||
}
|
||||
|
||||
66.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1);
|
||||
}
|
||||
|
||||
68.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1);
|
||||
}
|
||||
|
||||
71.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1);
|
||||
}
|
||||
|
||||
73.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1);
|
||||
}
|
||||
|
||||
76.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1);
|
||||
}
|
||||
|
||||
78.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1);
|
||||
}
|
||||
|
||||
81.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1);
|
||||
}
|
||||
|
||||
83.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1);
|
||||
}
|
||||
|
||||
86.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1);
|
||||
}
|
||||
|
||||
88.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
}
|
||||
|
||||
91.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
93.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1);
|
||||
}
|
||||
|
||||
96.666667% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1);
|
||||
}
|
||||
|
||||
98.333333% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-left.ns-hide {
|
||||
animation-name: animSlideLeft;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
0% {
|
||||
transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-show {
|
||||
animation: animSlideElasticRight 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticRight {
|
||||
0% {
|
||||
transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1);
|
||||
}
|
||||
|
||||
2.15% {
|
||||
transform: matrix3d(1.486, 0, 0, 0, 0, 0.514, 0, 0, 0, 0, 1, 0, 664.594, 0, 0, 1);
|
||||
}
|
||||
|
||||
4.1% {
|
||||
transform: matrix3d(1.147, 0, 0, 0, 0, 0.853, 0, 0, 0, 0, 1, 0, 419.708, 0, 0, 1);
|
||||
}
|
||||
|
||||
4.3% {
|
||||
transform: matrix3d(1.121, 0, 0, 0, 0, 0.879, 0, 0, 0, 0, 1, 0, 398.136, 0, 0, 1);
|
||||
}
|
||||
|
||||
6.46% {
|
||||
transform: matrix3d(0.948, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 206.714, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.11% {
|
||||
transform: matrix3d(0.908, 0, 0, 0, 0, 1.092, 0, 0, 0, 0, 1, 0, 105.491, 0, 0, 1);
|
||||
}
|
||||
|
||||
8.61% {
|
||||
transform: matrix3d(0.907, 0, 0, 0, 0, 1.093, 0, 0, 0, 0, 1, 0, 81.572, 0, 0, 1);
|
||||
}
|
||||
|
||||
12.11% {
|
||||
transform: matrix3d(0.95, 0, 0, 0, 0, 1.05, 0, 0, 0, 0, 1, 0, -18.434, 0, 0, 1);
|
||||
}
|
||||
|
||||
14.16% {
|
||||
transform: matrix3d(0.979, 0, 0, 0, 0, 1.021, 0, 0, 0, 0, 1, 0, -38.734, 0, 0, 1);
|
||||
}
|
||||
|
||||
16.12% {
|
||||
transform: matrix3d(0.997, 0, 0, 0, 0, 1.003, 0, 0, 0, 0, 1, 0, -43.356, 0, 0, 1);
|
||||
}
|
||||
|
||||
19.72% {
|
||||
transform: matrix3d(1.006, 0, 0, 0, 0, 0.994, 0, 0, 0, 0, 1, 0, -34.155, 0, 0, 1);
|
||||
}
|
||||
|
||||
27.23% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -7.839, 0, 0, 1);
|
||||
}
|
||||
|
||||
30.83% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1.951, 0, 0, 1);
|
||||
}
|
||||
|
||||
38.34% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.037, 0, 0, 1);
|
||||
}
|
||||
|
||||
41.99% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.812, 0, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.159, 0, 0, 1);
|
||||
}
|
||||
|
||||
60.56% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.025, 0, 0, 1);
|
||||
}
|
||||
|
||||
82.78% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.001, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-right.ns-hide {
|
||||
animation-name: animSlideRight;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideRight {
|
||||
0% {
|
||||
transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-show {
|
||||
animation: animSlideElasticCenter 2000ms linear both;
|
||||
}
|
||||
|
||||
@keyframes animSlideElasticCenter {
|
||||
0% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1);
|
||||
}
|
||||
|
||||
2.15% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.971, 0, 0, 0, 0, 1, 0, 0, -199.378, 0, 1);
|
||||
}
|
||||
|
||||
4.1% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.294, 0, 0, 0, 0, 1, 0, 0, -125.912, 0, 1);
|
||||
}
|
||||
|
||||
4.3% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.243, 0, 0, 0, 0, 1, 0, 0, -119.441, 0, 1);
|
||||
}
|
||||
|
||||
6.46% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.895, 0, 0, 0, 0, 1, 0, 0, -62.014, 0, 1);
|
||||
}
|
||||
|
||||
8.11% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.817, 0, 0, 0, 0, 1, 0, 0, -31.647, 0, 1);
|
||||
}
|
||||
|
||||
8.61% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.813, 0, 0, 0, 0, 1, 0, 0, -24.472, 0, 1);
|
||||
}
|
||||
|
||||
12.11% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 1, 0, 0, 5.53, 0, 1);
|
||||
}
|
||||
|
||||
14.16% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.959, 0, 0, 0, 0, 1, 0, 0, 11.62, 0, 1);
|
||||
}
|
||||
|
||||
16.12% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.994, 0, 0, 0, 0, 1, 0, 0, 13.007, 0, 1);
|
||||
}
|
||||
|
||||
19.72% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1.012, 0, 0, 0, 0, 1, 0, 0, 10.247, 0, 1);
|
||||
}
|
||||
|
||||
27.23% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 2.352, 0, 1);
|
||||
}
|
||||
|
||||
30.83% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0.585, 0, 1);
|
||||
}
|
||||
|
||||
38.34% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.311, 0, 1);
|
||||
}
|
||||
|
||||
41.99% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.244, 0, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.048, 0, 1);
|
||||
}
|
||||
|
||||
60.56% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.007, 0, 1);
|
||||
}
|
||||
|
||||
82.78% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-slide-center.ns-hide {
|
||||
animation-name: animSlideCenter;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes animSlideCenter {
|
||||
0% {
|
||||
transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ns-effect-genie.ns-show,
|
||||
.ns-effect-genie.ns-hide {
|
||||
animation-name: animGenie;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes animGenie {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 0.5;
|
||||
transform: translate3d(0, 0, 0) scale3d(0.02, 1.1, 1);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 0.6;
|
||||
transform: translate3d(0, -40px, 0) scale3d(0.8, 1.1, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,25 @@
|
||||
/**
|
||||
* Based on work by
|
||||
*
|
||||
* notificationFx.js v1.0.0
|
||||
* http://www.codrops.com
|
||||
* https://tympanus.net/codrops/
|
||||
*
|
||||
* Licensed under the MIT license.
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* https://opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* Copyright 2014, Codrops
|
||||
* http://www.codrops.com
|
||||
* https://tympanus.net/codrops/
|
||||
*/
|
||||
// jscs:disable
|
||||
;(function(window) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var docElem = window.document.documentElement,
|
||||
support = {animations: Modernizr.cssanimations},
|
||||
animEndEventNames = {
|
||||
"WebkitAnimation": "webkitAnimationEnd",
|
||||
"OAnimation": "oAnimationEnd",
|
||||
"msAnimation": "MSAnimationEnd",
|
||||
"animation": "animationend"
|
||||
},
|
||||
// animation end event name
|
||||
animEndEventName = animEndEventNames[ Modernizr.prefixed("animation") ];
|
||||
|
||||
(function (window) {
|
||||
/**
|
||||
* extend obj function
|
||||
* 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) {
|
||||
for (var key in b) {
|
||||
for (let key in b) {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
@@ -37,7 +28,10 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationFx function
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
function NotificationFx(options) {
|
||||
this.options = extend({}, this.options);
|
||||
@@ -70,19 +64,22 @@
|
||||
ttl: 6000,
|
||||
al_no: "ns-box",
|
||||
// callbacks
|
||||
onClose: function() { return false; },
|
||||
onOpen: function() { return false; }
|
||||
onClose: function () {
|
||||
return false;
|
||||
},
|
||||
onOpen: function () {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* init function
|
||||
* initialize and cache some vars
|
||||
* Initialize and cache some vars
|
||||
*/
|
||||
NotificationFx.prototype._init = function() {
|
||||
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;
|
||||
var 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;
|
||||
@@ -91,13 +88,12 @@
|
||||
this.options.wrapper.insertBefore(this.ntf, this.options.wrapper.nextSibling);
|
||||
|
||||
// dismiss after [options.ttl]ms
|
||||
var self = this;
|
||||
if (this.options.ttl) {
|
||||
this.dismissttl = setTimeout(function() {
|
||||
if (self.active) {
|
||||
self.dismiss();
|
||||
}
|
||||
}, this.options.ttl);
|
||||
this.dismissttl = setTimeout(() => {
|
||||
if (this.active) {
|
||||
this.dismiss();
|
||||
}
|
||||
}, this.options.ttl);
|
||||
}
|
||||
|
||||
// init events
|
||||
@@ -105,61 +101,58 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* init events
|
||||
* Init events
|
||||
*/
|
||||
NotificationFx.prototype._initEvents = function() {
|
||||
var self = this;
|
||||
NotificationFx.prototype._initEvents = function () {
|
||||
// dismiss notification by tapping on it if someone has a touchscreen
|
||||
this.ntf.querySelector(".ns-box-inner").addEventListener("click", function() { self.dismiss(); });
|
||||
this.ntf.querySelector(".ns-box-inner").addEventListener("click", () => {
|
||||
this.dismiss();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* show the notification
|
||||
* Show the notification
|
||||
*/
|
||||
NotificationFx.prototype.show = function() {
|
||||
NotificationFx.prototype.show = function () {
|
||||
this.active = true;
|
||||
classie.remove(this.ntf, "ns-hide");
|
||||
classie.add(this.ntf, "ns-show");
|
||||
this.ntf.classList.remove("ns-hide");
|
||||
this.ntf.classList.add("ns-show");
|
||||
this.options.onOpen();
|
||||
};
|
||||
|
||||
/**
|
||||
* dismiss the notification
|
||||
* Dismiss the notification
|
||||
*
|
||||
* @param {boolean} [close] call the onClose callback at the end
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function() {
|
||||
var self = this;
|
||||
NotificationFx.prototype.dismiss = function (close = true) {
|
||||
this.active = false;
|
||||
clearTimeout(this.dismissttl);
|
||||
classie.remove(this.ntf, "ns-show");
|
||||
setTimeout(function() {
|
||||
classie.add(self.ntf, "ns-hide");
|
||||
this.ntf.classList.remove("ns-show");
|
||||
setTimeout(() => {
|
||||
this.ntf.classList.add("ns-hide");
|
||||
|
||||
// callback
|
||||
self.options.onClose();
|
||||
if (close) this.options.onClose();
|
||||
}, 25);
|
||||
|
||||
// after animation ends remove ntf from the DOM
|
||||
var onEndAnimationFn = function(ev) {
|
||||
if (support.animations) {
|
||||
if (ev.target !== self.ntf) return false;
|
||||
this.removeEventListener(animEndEventName, onEndAnimationFn);
|
||||
const onEndAnimationFn = (ev) => {
|
||||
if (ev.target !== this.ntf) {
|
||||
return false;
|
||||
}
|
||||
this.ntf.removeEventListener("animationend", onEndAnimationFn);
|
||||
|
||||
if (this.parentNode === self.options.wrapper) {
|
||||
self.options.wrapper.removeChild(this);
|
||||
if (ev.target.parentNode === this.options.wrapper) {
|
||||
this.options.wrapper.removeChild(this.ntf);
|
||||
}
|
||||
};
|
||||
|
||||
if (support.animations) {
|
||||
this.ntf.addEventListener(animEndEventName, onEndAnimationFn);
|
||||
} else {
|
||||
onEndAnimationFn();
|
||||
}
|
||||
this.ntf.addEventListener("animationend", onEndAnimationFn);
|
||||
};
|
||||
|
||||
/**
|
||||
* add to global namespace
|
||||
* Add to global namespace
|
||||
*/
|
||||
window.NotificationFx = NotificationFx;
|
||||
|
||||
})(window);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror нотификация",
|
||||
"welcome": "Добре дошли, стартирането беше успешно"
|
||||
"sysTitle": "MagicMirror нотификация",
|
||||
"welcome": "Добре дошли, стартирането беше успешно"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notifikation",
|
||||
"welcome": "Velkommen, modulet er succesfuldt startet!"
|
||||
"sysTitle": "MagicMirror Notifikation",
|
||||
"welcome": "Velkommen, modulet er succesfuldt startet!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Benachrichtigung",
|
||||
"welcome": "Willkommen, Start war erfolgreich!"
|
||||
"sysTitle": "MagicMirror Benachrichtigung",
|
||||
"welcome": "Willkommen, Start war erfolgreich!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"welcome": "Welcome, start was successful!"
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"welcome": "Welcome, start was successful!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificaciones",
|
||||
"welcome": "Bienvenido, ¡se iniciado correctamente!"
|
||||
"sysTitle": "MagicMirror Notificaciones",
|
||||
"welcome": "Bienvenido, ¡se iniciado correctamente!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror értesítés",
|
||||
"welcome": "Üdvözöljük, indulás sikeres!"
|
||||
"sysTitle": "MagicMirror értesítés",
|
||||
"welcome": "Üdvözöljük, indulás sikeres!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificatie",
|
||||
"welcome": "Welkom, Succesvol gestart!"
|
||||
"sysTitle": "MagicMirror Notificatie",
|
||||
"welcome": "Welkom, Succesvol gestart!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Уведомление",
|
||||
"welcome": "Добро пожаловать, старт был успешным!"
|
||||
"sysTitle": "MagicMirror Уведомление",
|
||||
"welcome": "Добро пожаловать, старт был успешным!"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Module: Calendar
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
.calendar .symbol {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
font-size: 80%;
|
||||
vertical-align: top;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.calendar .symbol span {
|
||||
display: inline-block;
|
||||
-ms-transform: translate(0, 2px); /* IE 9 */
|
||||
-webkit-transform: translate(0, 2px); /* Safari */
|
||||
transform: translate(0, 2px);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.calendar .title {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,493 +1,156 @@
|
||||
/* Magic Mirror
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
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");
|
||||
|
||||
var ical = require("./vendor/ical.js");
|
||||
var moment = require("moment");
|
||||
/**
|
||||
*
|
||||
* @param {string} url The url of the calendar to fetch
|
||||
* @param {number} reloadInterval Time in ms the calendar is fetched again
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @class
|
||||
*/
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
let reloadTimer = null;
|
||||
let events = [];
|
||||
|
||||
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||
var self = this;
|
||||
let fetchFailedCallback = function () {};
|
||||
let eventsReceivedCallback = function () {};
|
||||
|
||||
var reloadTimer = null;
|
||||
var events = [];
|
||||
|
||||
var fetchFailedCallback = function() {};
|
||||
var eventsReceivedCallback = function() {};
|
||||
|
||||
/* fetchCalendar()
|
||||
/**
|
||||
* Initiates calendar fetch.
|
||||
*/
|
||||
var fetchCalendar = function() {
|
||||
|
||||
const fetchCalendar = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
|
||||
nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
var opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
},
|
||||
gzip: true
|
||||
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/)"
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
if (auth) {
|
||||
if(auth.method === "bearer"){
|
||||
opts.auth = {
|
||||
bearer: auth.pass
|
||||
};
|
||||
|
||||
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 });
|
||||
} else {
|
||||
opts.auth = {
|
||||
user: auth.user,
|
||||
pass: auth.pass
|
||||
};
|
||||
|
||||
if(auth.method === "digest"){
|
||||
opts.auth.sendImmediately = false;
|
||||
} else {
|
||||
opts.auth.sendImmediately = true;
|
||||
}
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
|
||||
}
|
||||
|
||||
ical.fromURL(url, opts, function(err, data) {
|
||||
if (err) {
|
||||
fetchFailedCallback(self, err);
|
||||
fetcher
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
maximumNumberOfDays
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(data);
|
||||
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
|
||||
var limitFunction = function(date, i) {return true;};
|
||||
|
||||
var eventDate = function(event, time) {
|
||||
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
for (var e in data) {
|
||||
var event = data[e];
|
||||
var now = new Date();
|
||||
var today = moment().startOf("day").toDate();
|
||||
var future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1,"seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
var past = today;
|
||||
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
// Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
var isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
|
||||
var startDate = eventDate(event, "start");
|
||||
var endDate;
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if(typeof event.duration !== "undefined") {
|
||||
dur=moment.duration(event.duration);
|
||||
endDate = startDate.clone().add(dur);
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
endDate = startDate;
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the duration f the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
var title = getTitleFromEvent(event);
|
||||
|
||||
var excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (var f in excludedEvents) {
|
||||
var filter = excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var location = event.location || false;
|
||||
var geo = event.geo || false;
|
||||
var description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
var rule = event.rrule;
|
||||
var addedEvents = 0;
|
||||
|
||||
// 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
|
||||
var pastLocal = moment(past).subtract(past.getTimezoneOffset(), "minutes").toDate();
|
||||
var futureLocal = moment(future).subtract(future.getTimezoneOffset(), "minutes").toDate();
|
||||
var datesLocal = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
var dates = datesLocal.map(function(dateLocal) {
|
||||
var date = moment(dateLocal).add(dateLocal.getTimezoneOffset(), "minutes").toDate();
|
||||
return date;
|
||||
});
|
||||
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it"s possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we"ll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don"t actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
if (event.recurrences != undefined)
|
||||
{
|
||||
var pastMoment = moment(past);
|
||||
var futureMoment = moment(future);
|
||||
|
||||
for (var 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 (var d in dates) {
|
||||
var date = dates[d];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
var dateKey = date.toISOString().substring(0,10);
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
|
||||
// Stop parsing this event's recurrences if we've already found maximumEntries worth of recurrences.
|
||||
// (The logic below would still filter the extras, but the check is simple since we're already tracking the count)
|
||||
if (addedEvents >= maximumEntries) {
|
||||
break;
|
||||
}
|
||||
|
||||
startDate = moment(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;
|
||||
}
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") == endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
var recurrenceTitle = 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 (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if ((showRecurrence === true) && (addedEvents < maximumEntries)) {
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
fullDayEvent: isFullDayEvent(event),
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// console.log("Single event ...");
|
||||
// Single event.
|
||||
var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event);
|
||||
|
||||
if (includePastEvents) {
|
||||
if (endDate < past) {
|
||||
//console.log("Past event is too far in the past. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (startDate > future) {
|
||||
//console.log("It exceeds the maximumNumberOfDays limit. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Every thing is good. Add it to the list.
|
||||
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newEvents.sort(function(a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
//console.log(newEvents);
|
||||
|
||||
events = newEvents.slice(0, maximumEntries);
|
||||
|
||||
self.broadcastEvents();
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
/* scheduleTimer()
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
var scheduleTimer = function() {
|
||||
//console.log('Schedule update timer.');
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function() {
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/* isFullDayEvent(event)
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
*/
|
||||
var isFullDayEvent = function(event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var start = event.start || 0;
|
||||
var startDate = new Date(start);
|
||||
var 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;
|
||||
};
|
||||
|
||||
/* timeFilterApplies()
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* argument now Date - Date object using previously created object for consistency
|
||||
* argument endDate Moment - Moment object representing the event end date
|
||||
* argument filter string - The time to subtract from the end date to determine if an event should be shown
|
||||
*
|
||||
* return bool - The event should be filtered out
|
||||
*/
|
||||
var timeFilterApplies = function(now, endDate, filter) {
|
||||
if (filter) {
|
||||
var until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice("-1") === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/* getTitleFromEvent(event)
|
||||
* Gets the title from the event.
|
||||
*
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return string - The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
var getTitleFromEvent = function (event) {
|
||||
var 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;
|
||||
};
|
||||
|
||||
var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
filter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
|
||||
filter = new RegExp(filter, regexFlags);
|
||||
|
||||
return filter.test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* startFetch()
|
||||
/**
|
||||
* Initiate fetchCalendar();
|
||||
*/
|
||||
this.startFetch = function() {
|
||||
this.startFetch = function () {
|
||||
fetchCalendar();
|
||||
};
|
||||
|
||||
/* broadcastItems()
|
||||
/**
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function() {
|
||||
//console.log('Broadcasting ' + events.length + ' events.');
|
||||
eventsReceivedCallback(self);
|
||||
this.broadcastEvents = function () {
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
/* onReceive(callback)
|
||||
/**
|
||||
* Sets the on success callback
|
||||
*
|
||||
* argument callback function - The on success callback.
|
||||
* @param {Function} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function(callback) {
|
||||
this.onReceive = function (callback) {
|
||||
eventsReceivedCallback = callback;
|
||||
};
|
||||
|
||||
/* onError(callback)
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* argument callback function - The on error callback.
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function(callback) {
|
||||
this.onError = function (callback) {
|
||||
fetchFailedCallback = callback;
|
||||
};
|
||||
|
||||
/* url()
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* return string - The url of this fetcher.
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function() {
|
||||
this.url = function () {
|
||||
return url;
|
||||
};
|
||||
|
||||
/* events()
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
*
|
||||
* return array - The current available events for this fetcher.
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function() {
|
||||
this.events = function () {
|
||||
return events;
|
||||
};
|
||||
};
|
||||
|
||||
623
modules/default/calendar/calendarutils.js
Normal file
623
modules/default/calendar/calendarutils.js
Normal file
@@ -0,0 +1,623 @@
|
||||
/* 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
|
||||
*/
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the events from ical according to the given config
|
||||
*
|
||||
* @param {object} data the calendar data from ical
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
// 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("\nEvent: " + JSON.stringify(event));
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug("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;
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug("event.recurrences: " + event.recurrences);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CalendarUtils;
|
||||
}
|
||||
@@ -2,38 +2,40 @@
|
||||
* 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 http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
var CalendarFetcher = require("./calendarfetcher.js");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
|
||||
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
// var url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
|
||||
var fetchInterval = 60 * 60 * 1000;
|
||||
var maximumEntries = 10;
|
||||
var maximumNumberOfDays = 365;
|
||||
var user = "magicmirror";
|
||||
var pass = "MyStrongPass";
|
||||
var broadcastPastEvents = false;
|
||||
|
||||
var auth = {
|
||||
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)
|
||||
const fetchInterval = 60 * 60 * 1000;
|
||||
const maximumEntries = 10;
|
||||
const maximumNumberOfDays = 365;
|
||||
const user = "magicmirror";
|
||||
const pass = "MyStrongPass";
|
||||
const auth = {
|
||||
user: user,
|
||||
pass: pass
|
||||
};
|
||||
|
||||
console.log("Create fetcher ...");
|
||||
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
|
||||
fetcher.onReceive(function(fetcher) {
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
console.log("------------------------------------------------------------");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
fetcher.onError(function(fetcher, error) {
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.log("Fetcher error:");
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
fetcher.startFetch();
|
||||
|
||||
@@ -1,79 +1,88 @@
|
||||
/* Magic Mirror
|
||||
* Node Helper: Calendar
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var NodeHelper = require("node_helper");
|
||||
var validUrl = require("valid-url");
|
||||
var CalendarFetcher = require("./calendarfetcher.js");
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function() {
|
||||
var events = [];
|
||||
|
||||
start: function () {
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
this.fetchers = [];
|
||||
|
||||
console.log("Starting node helper for: " + this.name);
|
||||
|
||||
},
|
||||
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function(notification, payload) {
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
//console.log('ADD_CALENDAR: ');
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
/* createFetcher(url, reloadInterval)
|
||||
/**
|
||||
* Creates a fetcher for a new url if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* @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.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
|
||||
* @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) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", {url: url});
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
Log.error("Calendar Error. Malformed calendar url: ", url, error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
let fetcher;
|
||||
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);
|
||||
|
||||
fetcher.onReceive(function(fetcher) {
|
||||
//console.log('Broadcast events.');
|
||||
//console.log(fetcher.events());
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
});
|
||||
|
||||
self.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", {
|
||||
id: identifier,
|
||||
error_type
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.onError(function(fetcher, error) {
|
||||
console.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
self.fetchers[url] = fetcher;
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
//console.log('Use existing news fetcher for url: ' + url);
|
||||
fetcher = self.fetchers[url];
|
||||
Log.log("Use existing calendarfetcher for url: " + url);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} fetcher the fetcher associated with the calendar
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents: function (fetcher, identifier) {
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8.9"
|
||||
install: npm install
|
||||
178
modules/default/calendar/vendor/ical.js/LICENSE
vendored
178
modules/default/calendar/vendor/ical.js/LICENSE
vendored
@@ -1,178 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
13
modules/default/calendar/vendor/ical.js/NOTICE
vendored
13
modules/default/calendar/vendor/ical.js/NOTICE
vendored
@@ -1,13 +0,0 @@
|
||||
Copyright 2012 Peter Braden
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
var ical = require('./node-ical')
|
||||
var moment = require('moment')
|
||||
|
||||
var data = ical.parseFile('./examples/example_rrule.ics');
|
||||
|
||||
// Complicated example demonstrating how to handle recurrence rules and exceptions.
|
||||
|
||||
for (var k in data) {
|
||||
|
||||
// When dealing with calendar recurrences, you need a range of dates to query against,
|
||||
// because otherwise you can get an infinite number of calendar events.
|
||||
var rangeStart = moment("2017-01-01");
|
||||
var rangeEnd = moment("2017-12-31");
|
||||
|
||||
|
||||
var event = data[k]
|
||||
if (event.type === 'VEVENT') {
|
||||
|
||||
var title = event.summary;
|
||||
var startDate = moment(event.start);
|
||||
var endDate = moment(event.end);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
// Simple case - no recurrences, just print out the calendar event.
|
||||
if (typeof event.rrule === 'undefined')
|
||||
{
|
||||
console.log('title:' + title);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(duration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Complicated case - if an RRULE exists, handle multiple recurrences of the event.
|
||||
else if (typeof event.rrule !== 'undefined')
|
||||
{
|
||||
// For recurring events, get the set of event start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
var dates = event.rrule.between(
|
||||
rangeStart.toDate(),
|
||||
rangeEnd.toDate(),
|
||||
true,
|
||||
function(date, i) {return true;}
|
||||
)
|
||||
|
||||
// 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. One way to handle this is
|
||||
// to add *all* recurrence override entries into the set of dates that we check, and then later
|
||||
// filter out any recurrences that don't actually belong within our range.
|
||||
if (event.recurrences != undefined)
|
||||
{
|
||||
for (var 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(rangeStart, rangeEnd) != true)
|
||||
{
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be printed.
|
||||
for(var i in dates) {
|
||||
|
||||
var date = dates[i];
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
var curDuration = duration;
|
||||
|
||||
startDate = moment(date);
|
||||
|
||||
// Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information)
|
||||
var dateLookupKey = date.toISOString().substring(0, 10);
|
||||
|
||||
// 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[dateLookupKey] != undefined))
|
||||
{
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateLookupKey];
|
||||
startDate = moment(curEvent.start);
|
||||
curDuration = 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[dateLookupKey] != undefined))
|
||||
{
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
// Set the the title and the end date from either the regular event or the recurrence override.
|
||||
var recurrenceTitle = curEvent.summary;
|
||||
endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x');
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range,
|
||||
// don't process it.
|
||||
if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
|
||||
console.log('title:' + recurrenceTitle);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(curDuration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:US/Central
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170601T090000
|
||||
DTEND;TZID=US/Central:20170601T170000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;BYDAY=TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Recurring weekly meeting from June 1 - Aug 14 (except July 6, July 13, July 20, Aug 3)
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
RECURRENCE-ID;TZID=US/Central:20170629T090000
|
||||
DTSTART;TZID=US/Central:20170703T090000
|
||||
DTEND;TZID=US/Central:20170703T120000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170216T143445Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Last meeting in June moved to Monday July 3 and shortened to half day
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:12354454-ABCD-DCBB-999A-2349872354897
|
||||
DTSTART;TZID=US/Central:20171201T130000
|
||||
DTEND;TZID=US/Central:20171201T150000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Single event on Dec 1
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
452
modules/default/calendar/vendor/ical.js/ical.js
vendored
452
modules/default/calendar/vendor/ical.js/ical.js
vendored
@@ -1,452 +0,0 @@
|
||||
(function(name, definition) {
|
||||
|
||||
/****************
|
||||
* A tolerant, minimal icalendar parser
|
||||
* (http://tools.ietf.org/html/rfc5545)
|
||||
*
|
||||
* <peterbraden@peterbraden.co.uk>
|
||||
* **************/
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = definition();
|
||||
} else if (typeof define === 'function' && typeof define.amd === 'object'){
|
||||
define(definition);
|
||||
} else {
|
||||
this[name] = definition();
|
||||
}
|
||||
|
||||
}('ical', function(){
|
||||
|
||||
// Unescape Text re RFC 4.3.11
|
||||
var text = function(t){
|
||||
t = t || "";
|
||||
return (t
|
||||
.replace(/\\\,/g, ',')
|
||||
.replace(/\\\;/g, ';')
|
||||
.replace(/\\[nN]/g, '\n')
|
||||
.replace(/\\\\/g, '\\')
|
||||
)
|
||||
}
|
||||
|
||||
var parseParams = function(p){
|
||||
var out = {}
|
||||
for (var i = 0; i<p.length; i++){
|
||||
if (p[i].indexOf('=') > -1){
|
||||
var segs = p[i].split('=');
|
||||
|
||||
out[segs[0]] = parseValue(segs.slice(1).join('='));
|
||||
|
||||
}
|
||||
}
|
||||
return out || sp
|
||||
}
|
||||
|
||||
var parseValue = function(val){
|
||||
if ('TRUE' === val)
|
||||
return true;
|
||||
|
||||
if ('FALSE' === val)
|
||||
return false;
|
||||
|
||||
var number = Number(val);
|
||||
if (!isNaN(number))
|
||||
return number;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
var storeValParam = function (name) {
|
||||
return function (val, curr) {
|
||||
var current = curr[name];
|
||||
if (Array.isArray(current)) {
|
||||
current.push(val);
|
||||
return curr;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
curr[name] = [current, val];
|
||||
return curr;
|
||||
}
|
||||
|
||||
curr[name] = val;
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
var storeParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var data;
|
||||
if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
|
||||
data = { params: parseParams(params), val: text(val) }
|
||||
}
|
||||
else
|
||||
data = text(val)
|
||||
|
||||
return storeValParam(name)(data, curr);
|
||||
}
|
||||
}
|
||||
|
||||
var addTZ = function (dt, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
dt.tz = p.TZID
|
||||
}
|
||||
|
||||
return dt
|
||||
}
|
||||
|
||||
var dateParam = function(name){
|
||||
return function (val, params, curr) {
|
||||
|
||||
var newDate = text(val);
|
||||
|
||||
|
||||
if (params && params[0] === "VALUE=DATE") {
|
||||
// Just Date
|
||||
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
|
||||
if (comps !== null) {
|
||||
// No TZ info - assume same timezone as this computer
|
||||
newDate = new Date(
|
||||
comps[1],
|
||||
parseInt(comps[2], 10)-1,
|
||||
comps[3]
|
||||
);
|
||||
|
||||
newDate = addTZ(newDate, params);
|
||||
newDate.dateOnly = true;
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//typical RFC date-time format
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||
if (comps !== null) {
|
||||
if (comps[7] == 'Z'){ // GMT
|
||||
newDate = new Date(Date.UTC(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10 )
|
||||
));
|
||||
// TODO add tz
|
||||
} else {
|
||||
newDate = new Date(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10)
|
||||
);
|
||||
}
|
||||
|
||||
newDate = addTZ(newDate, params);
|
||||
}
|
||||
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var geoParam = function(name){
|
||||
return function(val, params, curr){
|
||||
storeParam(val, params, curr)
|
||||
var parts = val.split(';');
|
||||
curr[name] = {lat:Number(parts[0]), lon:Number(parts[1])};
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
var categoriesParam = function (name) {
|
||||
var separatorPattern = /\s*,\s*/g;
|
||||
return function (val, params, curr) {
|
||||
storeParam(val, params, curr)
|
||||
if (curr[name] === undefined)
|
||||
curr[name] = val ? val.split(separatorPattern) : []
|
||||
else
|
||||
if (val)
|
||||
curr[name] = curr[name].concat(val.split(separatorPattern))
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
|
||||
// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
|
||||
// There can also be more than one EXDATE entries in a calendar record.
|
||||
// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
|
||||
// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
|
||||
// NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
|
||||
// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
|
||||
// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
|
||||
// 2. Daylight savings time potentially affects the time you would need to look up
|
||||
// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
|
||||
// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
|
||||
// ex: DTSTART:20170814T140000Z
|
||||
// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
// EXDATE:20171219T060000
|
||||
// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
|
||||
// TODO: See if this causes any problems with events that recur multiple times a day.
|
||||
var exdateParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var separatorPattern = /\s*,\s*/g;
|
||||
curr[name] = curr[name] || [];
|
||||
var dates = val ? val.split(separatorPattern) : [];
|
||||
dates.forEach(function (entry) {
|
||||
var exdate = new Array();
|
||||
dateParam(name)(entry, params, exdate);
|
||||
|
||||
if (exdate[name])
|
||||
{
|
||||
if (typeof exdate[name].toISOString === 'function') {
|
||||
curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
|
||||
} else {
|
||||
console.error("No toISOString function in exdate[name]", exdate[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
|
||||
// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
|
||||
var recurrenceParam = function (name) {
|
||||
return dateParam(name);
|
||||
}
|
||||
|
||||
var addFBType = function (fb, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
fb.type = p.FBTYPE || "BUSY"
|
||||
}
|
||||
|
||||
return fb;
|
||||
}
|
||||
|
||||
var freebusyParam = function (name) {
|
||||
return function(val, params, curr){
|
||||
var fb = addFBType({}, params);
|
||||
curr[name] = curr[name] || []
|
||||
curr[name].push(fb);
|
||||
|
||||
storeParam(val, params, fb);
|
||||
|
||||
var parts = val.split('/');
|
||||
|
||||
['start', 'end'].forEach(function (name, index) {
|
||||
dateParam(name)(parts[index], params, fb);
|
||||
});
|
||||
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
|
||||
objectHandlers : {
|
||||
'BEGIN' : function(component, params, curr, stack){
|
||||
stack.push(curr)
|
||||
|
||||
return {type:component, params:params}
|
||||
}
|
||||
|
||||
, 'END' : function(component, params, curr, stack){
|
||||
// prevents the need to search the root of the tree for the VCALENDAR object
|
||||
if (component === "VCALENDAR") {
|
||||
//scan all high level object in curr and drop all strings
|
||||
var key,
|
||||
obj;
|
||||
|
||||
for (key in curr) {
|
||||
if(curr.hasOwnProperty(key)) {
|
||||
obj = curr[key];
|
||||
if (typeof obj === 'string') {
|
||||
delete curr[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return curr
|
||||
}
|
||||
|
||||
var par = stack.pop()
|
||||
|
||||
if (curr.uid)
|
||||
{
|
||||
// If this is the first time we run into this UID, just save it.
|
||||
if (par[curr.uid] === undefined)
|
||||
{
|
||||
par[curr.uid] = curr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we have multiple ical entries with the same UID, it's either going to be a
|
||||
// modification to a recurrence (RECURRENCE-ID), and/or a significant modification
|
||||
// to the entry (SEQUENCE).
|
||||
|
||||
// TODO: Look into proper sequence logic.
|
||||
|
||||
if (curr.recurrenceid === undefined)
|
||||
{
|
||||
// If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
|
||||
// not quite sure what the correct behaviour should be. For now, just take the new information
|
||||
// and merge it with the old record by overwriting only the fields that appear in the new record.
|
||||
var key;
|
||||
for (key in curr) {
|
||||
par[curr.uid][key] = curr[key];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
|
||||
// To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
|
||||
// array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
|
||||
// for that day.
|
||||
|
||||
// NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
|
||||
// case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
|
||||
// in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
|
||||
// fields in the parent record.
|
||||
|
||||
if (curr.recurrenceid != null)
|
||||
{
|
||||
|
||||
// TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
|
||||
|
||||
// Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
|
||||
// except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
|
||||
// would end up with a shared reference that would cause us to overwrite *both* records at the point
|
||||
// that we try and fix up the parent record.)
|
||||
var recurrenceObj = new Object();
|
||||
var key;
|
||||
for (key in curr) {
|
||||
recurrenceObj[key] = curr[key];
|
||||
}
|
||||
|
||||
if (recurrenceObj.recurrences != undefined) {
|
||||
delete recurrenceObj.recurrences;
|
||||
}
|
||||
|
||||
|
||||
// If we don't have an array to store recurrences in yet, create it.
|
||||
if (par[curr.uid].recurrences === undefined) {
|
||||
par[curr.uid].recurrences = new Array();
|
||||
}
|
||||
|
||||
// Save off our cloned recurrence object into the array, keyed by date but not time.
|
||||
// We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
|
||||
// TODO: See if this causes a problem with events that have multiple recurrences per day.
|
||||
if (typeof curr.recurrenceid.toISOString === 'function') {
|
||||
par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
|
||||
} else {
|
||||
console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
|
||||
}
|
||||
}
|
||||
|
||||
// One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
|
||||
// let's make sure to clear the recurrenceid off the parent field.
|
||||
if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
|
||||
{
|
||||
delete par[curr.uid].recurrenceid;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
|
||||
|
||||
return par
|
||||
}
|
||||
|
||||
, 'SUMMARY' : storeParam('summary')
|
||||
, 'DESCRIPTION' : storeParam('description')
|
||||
, 'URL' : storeParam('url')
|
||||
, 'UID' : storeParam('uid')
|
||||
, 'LOCATION' : storeParam('location')
|
||||
, 'DTSTART' : dateParam('start')
|
||||
, 'DTEND' : dateParam('end')
|
||||
, 'EXDATE' : exdateParam('exdate')
|
||||
,' CLASS' : storeParam('class')
|
||||
, 'TRANSP' : storeParam('transparency')
|
||||
, 'GEO' : geoParam('geo')
|
||||
, 'PERCENT-COMPLETE': storeParam('completion')
|
||||
, 'COMPLETED': dateParam('completed')
|
||||
, 'CATEGORIES': categoriesParam('categories')
|
||||
, 'FREEBUSY': freebusyParam('freebusy')
|
||||
, 'DTSTAMP': dateParam('dtstamp')
|
||||
, 'CREATED': dateParam('created')
|
||||
, 'LAST-MODIFIED': dateParam('lastmodified')
|
||||
, 'RECURRENCE-ID': recurrenceParam('recurrenceid')
|
||||
|
||||
},
|
||||
|
||||
|
||||
handleObject : function(name, val, params, ctx, stack, line){
|
||||
var self = this
|
||||
|
||||
if(self.objectHandlers[name])
|
||||
return self.objectHandlers[name](val, params, ctx, stack, line)
|
||||
|
||||
//handling custom properties
|
||||
if(name.match(/X\-[\w\-]+/) && stack.length > 0) {
|
||||
//trimming the leading and perform storeParam
|
||||
name = name.substring(2);
|
||||
return (storeParam(name))(val, params, ctx, stack, line);
|
||||
}
|
||||
|
||||
return storeParam(name.toLowerCase())(val, params, ctx);
|
||||
},
|
||||
|
||||
|
||||
parseICS : function(str){
|
||||
var self = this
|
||||
var lines = str.split(/\r?\n/)
|
||||
var ctx = {}
|
||||
var stack = []
|
||||
|
||||
for (var i = 0, ii = lines.length, l = lines[0]; i<ii; i++, l=lines[i]){
|
||||
//Unfold : RFC#3.1
|
||||
while (lines[i+1] && /[ \t]/.test(lines[i+1][0])) {
|
||||
l += lines[i+1].slice(1)
|
||||
i += 1
|
||||
}
|
||||
|
||||
var kv = l.split(":")
|
||||
|
||||
if (kv.length < 2){
|
||||
// Invalid line - must have k&v
|
||||
continue;
|
||||
}
|
||||
|
||||
// Although the spec says that vals with colons should be quote wrapped
|
||||
// in practise nobody does, so we assume further colons are part of the
|
||||
// val
|
||||
var value = kv.slice(1).join(":")
|
||||
, kp = kv[0].split(";")
|
||||
, name = kp[0]
|
||||
, params = kp.slice(1)
|
||||
|
||||
ctx = self.handleObject(name, value, params, ctx, stack, l) || {}
|
||||
}
|
||||
|
||||
// type and params are added to the list of items, get rid of them.
|
||||
delete ctx.type
|
||||
delete ctx.params
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
}
|
||||
}))
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = require('./ical')
|
||||
|
||||
var node = require('./node-ical')
|
||||
|
||||
// Copy node functions across to exports
|
||||
for (var i in node){
|
||||
module.exports[i] = node[i]
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
var ical = require('./ical')
|
||||
, request = require('request')
|
||||
, fs = require('fs')
|
||||
|
||||
exports.fromURL = function(url, opts, cb){
|
||||
if (!cb)
|
||||
return;
|
||||
request(url, opts, function(err, r, data){
|
||||
if (err)
|
||||
{
|
||||
return cb(err, null);
|
||||
}
|
||||
else if (r.statusCode != 200)
|
||||
{
|
||||
return cb(r.statusCode + ": " + r.statusMessage, null);
|
||||
}
|
||||
|
||||
cb(undefined, ical.parseICS(data));
|
||||
})
|
||||
}
|
||||
|
||||
exports.parseFile = function(filename){
|
||||
return ical.parseICS(fs.readFileSync(filename, 'utf8'))
|
||||
}
|
||||
|
||||
|
||||
var rrule = require('rrule').RRule
|
||||
|
||||
function getLocaleISOString(date) {
|
||||
var year = date.getFullYear().toString(10).padStart(4,'0');
|
||||
var month = (date.getMonth() + 1).toString(10).padStart(2,'0');
|
||||
var day = date.getDate().toString(10).padStart(2,'0');
|
||||
var hour = date.getHours().toString(10).padStart(2,'0');
|
||||
var minute = date.getMinutes().toString(10).padStart(2,'0');
|
||||
var second = date.getSeconds().toString(10).padStart(2,'0');
|
||||
|
||||
return `${year}${month}${day}T${hour}${minute}${second}Z`;
|
||||
}
|
||||
|
||||
ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
|
||||
curr.rrule = line;
|
||||
return curr
|
||||
}
|
||||
var originalEnd = ical.objectHandlers['END'];
|
||||
ical.objectHandlers['END'] = function (val, params, curr, stack) {
|
||||
// Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
|
||||
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
|
||||
// due to the subtypes.
|
||||
if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) {
|
||||
if (curr.rrule) {
|
||||
var rule = curr.rrule.replace('RRULE:', '');
|
||||
if (rule.indexOf('DTSTART') === -1) {
|
||||
|
||||
if (curr.start.length === 8) {
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start);
|
||||
if (comps) {
|
||||
curr.start = new Date(comps[1], comps[2] - 1, comps[3]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof curr.start.toISOString === 'function') {
|
||||
try {
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
rule += ';DTSTART=' + getLocaleISOString(curr.start);
|
||||
} catch (error) {
|
||||
console.error("ERROR when trying to convert to ISOString", error);
|
||||
}
|
||||
} else {
|
||||
console.error("No toISOString function in curr.start", curr.start);
|
||||
}
|
||||
}
|
||||
curr.rrule = rrule.fromString(rule);
|
||||
}
|
||||
}
|
||||
return originalEnd.call(this, val, params, curr, stack);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "ical",
|
||||
"version": "0.5.0",
|
||||
"main": "index.js",
|
||||
"description": "A tolerant, minimal icalendar parser",
|
||||
"keywords": [
|
||||
"ical",
|
||||
"ics",
|
||||
"calendar"
|
||||
],
|
||||
"homepage": "https://github.com/peterbraden/ical.js",
|
||||
"author": "Peter Braden <peterbraden@peterbraden.co.uk> (peterbraden.co.uk)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/peterbraden/ical.js.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"rrule": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vows": "0.8.2",
|
||||
"underscore": "1.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/vows/bin/vows ./test/test.js"
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
# ical.js #
|
||||
(Formerly node-ical)
|
||||
|
||||
[](https://travis-ci.org/peterbraden/ical.js)
|
||||
|
||||
A tolerant, minimal icalendar parser for javascript/node
|
||||
(http://tools.ietf.org/html/rfc5545)
|
||||
|
||||
|
||||
|
||||
## Install - Node.js ##
|
||||
|
||||
ical.js is availble on npm:
|
||||
|
||||
npm install ical
|
||||
|
||||
|
||||
|
||||
## API ##
|
||||
|
||||
ical.parseICS(str)
|
||||
|
||||
Parses a string with an ICS File
|
||||
|
||||
var data = ical.parseFile(filename)
|
||||
|
||||
Reads in the specified iCal file, parses it and returns the parsed data
|
||||
|
||||
ical.fromURL(url, options, function(err, data) {} )
|
||||
|
||||
Use the request library to fetch the specified URL (```opts``` gets passed on to the ```request()``` call), and call the function with the result (either an error or the data).
|
||||
|
||||
|
||||
|
||||
## Example 1 - Print list of upcoming node conferences (see example.js)
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Recurrences and Exceptions ##
|
||||
Calendar events with recurrence rules can be significantly more complicated to handle correctly. There are three parts to handling them:
|
||||
|
||||
1. rrule - the recurrence rule specifying the pattern of recurring dates and times for the event.
|
||||
2. recurrences - an optional array of event data that can override specific occurrences of the event.
|
||||
3. exdate - an optional array of dates that should be excluded from the recurrence pattern.
|
||||
|
||||
See example_rrule.js for an example of handling recurring calendar events.
|
||||
500
modules/default/calendar/vendor/ical.js/test/test.js
vendored
500
modules/default/calendar/vendor/ical.js/test/test.js
vendored
@@ -1,500 +0,0 @@
|
||||
/****
|
||||
* Tests
|
||||
*
|
||||
*
|
||||
***/
|
||||
process.env.TZ = 'America/San_Francisco';
|
||||
var ical = require('../index')
|
||||
|
||||
var vows = require('vows')
|
||||
, assert = require('assert')
|
||||
, _ = require('underscore')
|
||||
|
||||
vows.describe('node-ical').addBatch({
|
||||
'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test1.ics')
|
||||
}
|
||||
|
||||
,'we get 9 events': function (topic) {
|
||||
var events = _.select(_.values(topic), function(x){ return x.type==='VEVENT'})
|
||||
assert.equal (events.length, 9);
|
||||
}
|
||||
|
||||
,'event 47f6e' : {
|
||||
topic: function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid ==='47f6ea3f28af2986a2192fa39a91fa7d60d26b76'})[0]
|
||||
}
|
||||
,'is in fort lauderdale' : function(topic){
|
||||
assert.equal(topic.location, "Fort Lauderdale, United States")
|
||||
}
|
||||
,'starts Tue, 29 Nov 2011' : function(topic){
|
||||
assert.equal(topic.start.toDateString(), new Date(2011,10,29).toDateString())
|
||||
}
|
||||
}
|
||||
, 'event 480a' : {
|
||||
topic: function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid ==='480a3ad48af5ed8965241f14920f90524f533c18'})[0]
|
||||
}
|
||||
, 'has a summary (invalid colon handling tolerance)' : function(topic){
|
||||
assert.equal(topic.summary, '[Async]: Everything Express')
|
||||
}
|
||||
, 'has a date only start datetime' : function(topic){
|
||||
assert.equal(topic.start.dateOnly, true)
|
||||
}
|
||||
, 'has a date only end datetime' : function(topic){
|
||||
assert.equal(topic.end.dateOnly, true)
|
||||
}
|
||||
}
|
||||
, 'event d4c8' :{
|
||||
topic : function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'})[0]
|
||||
}
|
||||
, 'has a start datetime' : function(topic){
|
||||
assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString())
|
||||
}
|
||||
}
|
||||
|
||||
, 'event sdfkf09fsd0 (Invalid Date)' :{
|
||||
topic : function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid === 'sdfkf09fsd0'})[0]
|
||||
}
|
||||
, 'has a start datetime' : function(topic){
|
||||
assert.equal(topic.start, "Next Year")
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test2.ics (testing ical features)' : {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test2.ics')
|
||||
}
|
||||
, 'todo item uid4@host1.com' : {
|
||||
topic : function(items){
|
||||
return items['uid4@host1.com']
|
||||
}
|
||||
, 'is a VTODO' : function(topic){
|
||||
assert.equal(topic.type, 'VTODO')
|
||||
}
|
||||
}
|
||||
, 'vfreebusy' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.type === 'VFREEBUSY';
|
||||
})[0];
|
||||
}
|
||||
, 'has a URL' : function(topic) {
|
||||
assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb');
|
||||
}
|
||||
}
|
||||
, 'vfreebusy first freebusy' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.type === 'VFREEBUSY';
|
||||
})[0].freebusy[0];
|
||||
}
|
||||
, 'has undefined type defaulting to busy' : function(topic) {
|
||||
assert.equal(topic.type, "BUSY");
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 1998);
|
||||
assert.equal(topic.start.getUTCMonth(), 2);
|
||||
assert.equal(topic.start.getUTCDate(), 14);
|
||||
assert.equal(topic.start.getUTCHours(), 23);
|
||||
assert.equal(topic.start.getUTCMinutes(), 30);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 1998);
|
||||
assert.equal(topic.end.getUTCMonth(), 2);
|
||||
assert.equal(topic.end.getUTCDate(), 15);
|
||||
assert.equal(topic.end.getUTCHours(), 00);
|
||||
assert.equal(topic.end.getUTCMinutes(), 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test3.ics (testing tvcountdown.com)' : {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test3.ics');
|
||||
}
|
||||
, 'event -83' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === '20110505T220000Z-83@tvcountdown.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2011);
|
||||
assert.equal(topic.start.getMonth(), 4);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2011);
|
||||
assert.equal(topic.end.getMonth(), 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test4.ics (testing tripit.com)' : {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test4.ics');
|
||||
}
|
||||
, 'event c32a5...' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2011);
|
||||
assert.equal(topic.start.getMonth(), 09);
|
||||
assert.equal(topic.start.getDate(), 11);
|
||||
}
|
||||
|
||||
, 'has a summary' : function(topic){
|
||||
// escaped commas and semicolons should be replaced
|
||||
assert.equal(topic.summary, 'South San Francisco, CA, October 2011;')
|
||||
|
||||
}
|
||||
|
||||
, 'has a description' : function(topic){
|
||||
var desired = 'John Doe is in South San Francisco, CA from Oct 11 ' +
|
||||
'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' +
|
||||
'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' +
|
||||
'it.com\n'
|
||||
assert.equal(topic.description, desired)
|
||||
|
||||
}
|
||||
|
||||
, 'has a geolocation' : function(topic){
|
||||
assert.ok(topic.geo, 'no geo param')
|
||||
assert.equal(topic.geo.lat, 37.654656)
|
||||
assert.equal(topic.geo.lon, -122.40775)
|
||||
}
|
||||
|
||||
, 'has transparency' : function(topic){
|
||||
assert.equal(topic.transparency, 'TRANSPARENT')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
, 'with test5.ics (testing meetup.com)' : {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test5.ics')
|
||||
}
|
||||
, 'event nsmxnyppbfc@meetup.com' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === 'event_nsmxnyppbfc@meetup.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.tz, 'America/Phoenix')
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test6.ics (testing assembly.org)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test6.ics')
|
||||
}
|
||||
, 'event with no ID' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.summary === 'foobar Summer 2011 starts!';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
, 'event with rrule' :{
|
||||
topic: function(events){
|
||||
return _.select(_.values(events), function(x){
|
||||
return x.summary === "foobarTV broadcast starts"
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function(topic){
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "RRule text": function(topic){
|
||||
assert.equal(topic.rrule.toText(), "every 5 weeks on Monday, Friday until January 30, 2013")
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test7.ics (testing dtstart of rrule)' :{
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test7.ics');
|
||||
},
|
||||
'recurring yearly event (14 july)': {
|
||||
topic: function(events){
|
||||
var ev = _.values(events)[0];
|
||||
return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1));
|
||||
},
|
||||
'dt start well set': function(topic) {
|
||||
assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString());
|
||||
}
|
||||
}
|
||||
}
|
||||
, "with test 8.ics (VTODO completion)": {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test8.ics');
|
||||
},
|
||||
'grabbing VTODO task': {
|
||||
topic: function(topic) {
|
||||
return _.values(topic)[0];
|
||||
},
|
||||
'task completed': function(task){
|
||||
assert.equal(task.completion, 100);
|
||||
assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString());
|
||||
}
|
||||
}
|
||||
}
|
||||
, "with test 9.ics (VEVENT with VALARM)": {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test9.ics');
|
||||
},
|
||||
'grabbing VEVENT task': {
|
||||
topic: function(topic) {
|
||||
return _.values(topic)[0];
|
||||
},
|
||||
'task completed': function(task){
|
||||
assert.equal(task.summary, "Event with an alarm");
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test 11.ics (VEVENT with custom properties)': {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test10.ics');
|
||||
},
|
||||
'grabbing custom properties': {
|
||||
topic: function(topic) {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'with test10.ics': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test10.ics');
|
||||
},
|
||||
|
||||
'when categories present': {
|
||||
topic: function (t) {return _.values(t)[0]},
|
||||
|
||||
'should be a list': function (e) {
|
||||
assert(e.categories instanceof [].constructor);
|
||||
},
|
||||
|
||||
'should contain individual category values': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present with trailing whitespace': {
|
||||
topic: function (t) {return _.values(t)[1]},
|
||||
|
||||
'should contain individual category values without whitespace': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present but empty': {
|
||||
topic: function (t) {return _.values(t)[2]},
|
||||
|
||||
'should be an empty list': function (e) {
|
||||
assert.deepEqual(e.categories, []);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present but singular': {
|
||||
topic: function (t) {return _.values(t)[3]},
|
||||
|
||||
'should be a list of single item': function (e) {
|
||||
assert.deepEqual(e.categories, ['lonely-cat']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present on multiple lines': {
|
||||
topic: function (t) {return _.values(t)[4]},
|
||||
|
||||
'should contain the category values in an array': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'with test11.ics (testing zimbra freebusy)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test11.ics');
|
||||
},
|
||||
|
||||
'freebusy params' : {
|
||||
topic: function(events) {
|
||||
return _.values(events)[0];
|
||||
}
|
||||
, 'has a URL' : function(topic) {
|
||||
assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416');
|
||||
}
|
||||
, 'has an ORGANIZER' : function(topic) {
|
||||
assert.equal(topic.organizer, 'mailto:yvr-2a@example.com');
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2014);
|
||||
assert.equal(topic.start.getMonth(), 3);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2014);
|
||||
assert.equal(topic.end.getMonth(), 6);
|
||||
}
|
||||
}
|
||||
, 'freebusy busy events' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events)[0].freebusy, function(x) {
|
||||
return x.type === 'BUSY';
|
||||
})[0];
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2014);
|
||||
assert.equal(topic.start.getMonth(), 3);
|
||||
assert.equal(topic.start.getUTCHours(), 15);
|
||||
assert.equal(topic.start.getUTCMinutes(), 15);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2014);
|
||||
assert.equal(topic.end.getMonth(), 3);
|
||||
assert.equal(topic.end.getUTCHours(), 19);
|
||||
assert.equal(topic.end.getUTCMinutes(), 00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test12.ics (testing recurrences and exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test12.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '0000001';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary Treasure Hunting": function (topic) {
|
||||
assert.equal(topic.summary, 'Treasure Hunting');
|
||||
}
|
||||
, "Has two EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)].summary, 'More Treasure Hunting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test13.ics (testing recurrence-id before rrule)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test13.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary 'repeated'": function (topic) {
|
||||
assert.equal(topic.summary, 'repeated');
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)].summary, 'bla bla');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing comma-separated exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with comma-separated exdate': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '98765432-ABCD-DCBB-999A-987765432123';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of comma-separated exdates'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of comma-separated exdates');
|
||||
}
|
||||
, "Has four comma-separated EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the four comma-separated EXDATES are there
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
// Verify an arbitrary date isn't there
|
||||
assert.equal(topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing exdates with bad times)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with exdates with bad times': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of exdate with bad times'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of exdate with bad times');
|
||||
}
|
||||
, "Has two EXDATES even though they have bad times": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the two EXDATES are there, even though they have bad times
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'url request errors': {
|
||||
topic : function () {
|
||||
ical.fromURL('http://255.255.255.255/', {}, this.callback);
|
||||
}
|
||||
, 'are passed back to the callback' : function (err, result) {
|
||||
assert.instanceOf(err, Error);
|
||||
if (!err){
|
||||
console.log(">E:", err, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).export(module)
|
||||
|
||||
|
||||
//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics',
|
||||
// {},
|
||||
// function(err, data){
|
||||
// console.log("OUT:", data)
|
||||
// })
|
||||
@@ -1,78 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//lanyrd.com//Lanyrd//EN
|
||||
X-ORIGINAL-URL:http://lanyrd.com/topics/nodejs/nodejs.ics
|
||||
X-WR-CALNAME;CHARSET=utf-8:Node.js conferences
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Dyncon 2011
|
||||
LOCATION;CHARSET=utf-8:Stockholm, Sweden
|
||||
URL:http://lanyrd.com/2011/dyncon/
|
||||
UID:d4c826dfb701f611416d69b4df81caf9ff80b03a
|
||||
DTSTART:20110312T200000Z
|
||||
DTEND;VALUE=DATE:20110314
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:[Async]: Everything Express
|
||||
LOCATION;CHARSET=utf-8:Brighton, United Kingdom
|
||||
URL:http://lanyrd.com/2011/asyncjs-express/
|
||||
UID:480a3ad48af5ed8965241f14920f90524f533c18
|
||||
DTSTART;VALUE=DATE:20110324
|
||||
DTEND;VALUE=DATE:20110325
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:JSConf US 2011
|
||||
LOCATION;CHARSET=utf-8:Portland, United States
|
||||
URL:http://lanyrd.com/2011/jsconf/
|
||||
UID:ed334cc85db5ebdff5ff5a630a7a48631a677dbe
|
||||
DTSTART;VALUE=DATE:20110502
|
||||
DTEND;VALUE=DATE:20110504
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:NodeConf 2011
|
||||
LOCATION;CHARSET=utf-8:Portland, United States
|
||||
URL:http://lanyrd.com/2011/nodeconf/
|
||||
UID:25169a7b1ba5c248278f47120a40878055dc8c15
|
||||
DTSTART;VALUE=DATE:20110505
|
||||
DTEND;VALUE=DATE:20110506
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:BrazilJS
|
||||
LOCATION;CHARSET=utf-8:Fortaleza, Brazil
|
||||
URL:http://lanyrd.com/2011/braziljs/
|
||||
UID:dafee3be83624f3388c5635662229ff11766bb9c
|
||||
DTSTART;VALUE=DATE:20110513
|
||||
DTEND;VALUE=DATE:20110515
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Falsy Values
|
||||
LOCATION;CHARSET=utf-8:Warsaw, Poland
|
||||
URL:http://lanyrd.com/2011/falsy-values/
|
||||
UID:73cad6a09ac4e7310979c6130f871d17d990b5ad
|
||||
DTSTART;VALUE=DATE:20110518
|
||||
DTEND;VALUE=DATE:20110521
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:nodecamp.eu
|
||||
LOCATION;CHARSET=utf-8:Cologne, Germany
|
||||
URL:http://lanyrd.com/2011/nodecampde/
|
||||
UID:b728a5fdb5f292b6293e4a2fd97a1ccfc69e9d6f
|
||||
DTSTART;VALUE=DATE:20110611
|
||||
DTEND;VALUE=DATE:20110613
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Rich Web Experience 2011
|
||||
LOCATION;CHARSET=utf-8:Fort Lauderdale, United States
|
||||
URL:http://lanyrd.com/2011/rich-web-experience/
|
||||
UID:47f6ea3f28af2986a2192fa39a91fa7d60d26b76
|
||||
DTSTART;VALUE=DATE:20111129
|
||||
DTEND;VALUE=DATE:20111203
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Foobar
|
||||
UID:sdfkf09fsd0
|
||||
DTSTART;VALUE=DATE:Next Year
|
||||
DTEND;VALUE=DATE:20111203
|
||||
END:VEVENT
|
||||
|
||||
END:VCALENDAR
|
||||
@@ -1,34 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:1
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1,cat2,cat3
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:2
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1 , cat2, cat3
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:3
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:4
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:lonely-cat
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:5
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1
|
||||
CATEGORIES:cat2
|
||||
CATEGORIES:cat3
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,41 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VFREEBUSY
|
||||
ORGANIZER:mailto:yvr-2a@example.com
|
||||
DTSTAMP:20140516T235436Z
|
||||
DTSTART:20140415T235436Z
|
||||
DTEND:20140717T235436Z
|
||||
URL:http://mail.example.com/yvr-2a@example.com/20140416
|
||||
FREEBUSY;FBTYPE=BUSY:20140416T151500Z/20140416T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140416T195500Z/20140416T231500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140417T193000Z/20140417T203000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140421T210000Z/20140421T213000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T180000Z/20140423T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T200000Z/20140423T210000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T223500Z/20140423T231500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T155000Z/20140424T165500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T170000Z/20140424T183000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T195000Z/20140424T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T144500Z/20140425T161500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T180000Z/20140425T194500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T223000Z/20140425T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T151500Z/20140428T163000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T170000Z/20140428T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T195500Z/20140428T213000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T231000Z/20140428T234000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T152500Z/20140429T170000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T180000Z/20140429T183000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T201500Z/20140429T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140430T162500Z/20140430T165500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140430T180000Z/20140430T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T170000Z/20140501T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T175000Z/20140501T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T232000Z/20140501T235000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140502T163500Z/20140502T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T165500Z/20140505T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T201500Z/20140505T203000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T210000Z/20140505T213000Z
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
||||
@@ -1,19 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:Treasure Hunting
|
||||
DTSTART;TZID=America/Los_Angeles:20150706T120000
|
||||
DTEND;TZID=America/Los_Angeles:20150706T130000
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
EXDATE;TZID=America/Los_Angeles:20150708T120000
|
||||
EXDATE;TZID=America/Los_Angeles:20150710T120000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:More Treasure Hunting
|
||||
LOCATION:The other island
|
||||
DTSTART;TZID=America/Los_Angeles:20150709T150000
|
||||
DTEND;TZID=America/Los_Angeles:20150707T160000
|
||||
RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,57 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Kiev
|
||||
X-LIC-LOCATION:Europe/Kiev
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0300
|
||||
TZNAME:EEST
|
||||
DTSTART:19700329T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0300
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:EET
|
||||
DTSTART:19701025T040000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160826T140000
|
||||
DTEND;TZID=Europe/Kiev:20160826T150000
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
RECURRENCE-ID;TZID=Europe/Kiev:20160826T140000
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T130320Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:bla bla
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160825T140000
|
||||
DTEND;TZID=Europe/Kiev:20160825T150000
|
||||
RRULE:FREQ=DAILY;UNTIL=20160828T110000Z
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T125221Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:repeated
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,33 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170216T090000
|
||||
DTEND;TZID=US/Central:20170216T190000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170717T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;INTERVAL=2;BYDAY=MO,TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of comma-separated exdates
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:1234567-ABCD-ABCD-ABCD-123456789012
|
||||
DTSTART:20170814T140000Z
|
||||
DTEND:20170815T000000Z
|
||||
DTSTAMP:20171204T134925Z
|
||||
EXDATE:20171219T060000
|
||||
EXDATE:20171218T060000
|
||||
LAST-MODIFIED:20171024T140004Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of exdate with bad times
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,83 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
|
||||
METHOD:PUBLISH
|
||||
PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
|
||||
X-WR-CALNAME;VALUE=TEXT:Example
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:5
|
||||
DTSTART;TZID=US/Pacific:20021028T140000
|
||||
DTSTAMP:20021028T011706Z
|
||||
SUMMARY:Coffee with Jason
|
||||
UID:EC9439B1-FF65-11D6-9973-003065F99D04
|
||||
DTEND;TZID=US/Pacific:20021028T150000
|
||||
END:VEVENT
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DURATION:-P1D
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event reminder
|
||||
END:VALARM
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20021128T012034Z
|
||||
SUMMARY:Code Review
|
||||
UID:EC944331-FF65-11D6-9973-003065F99D04
|
||||
DTSTART;TZID=US/Pacific:20021127T120000
|
||||
DURATION:PT1H
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20021028T012034Z
|
||||
SUMMARY:Dinner with T
|
||||
UID:EC944CFA-FF65-11D6-9973-003065F99D04
|
||||
DTSTART;TZID=US/Pacific:20021216T200000
|
||||
DURATION:PT1H
|
||||
END:VEVENT
|
||||
BEGIN:VTODO
|
||||
DTSTAMP:19980130T134500Z
|
||||
SEQUENCE:2
|
||||
UID:uid4@host1.com
|
||||
ORGANIZER:MAILTO:unclesam@us.gov
|
||||
ATTENDEE;PARTSTAT=ACCEPTED:MAILTO:jqpublic@host.com
|
||||
DUE:19980415T235959
|
||||
STATUS:NEEDS-ACTION
|
||||
SUMMARY:Submit Income Taxes
|
||||
END:VTODO
|
||||
BEGIN:VALARM
|
||||
ACTION:AUDIO
|
||||
TRIGGER:19980403T120000
|
||||
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/audio-
|
||||
files/ssbanner.aud
|
||||
REPEAT:4
|
||||
DURATION:PT1H
|
||||
END:VALARM
|
||||
BEGIN:VJOURNAL
|
||||
DTSTAMP:19970324T120000Z
|
||||
UID:uid5@host1.com
|
||||
ORGANIZER:MAILTO:jsmith@host.com
|
||||
STATUS:DRAFT
|
||||
CLASS:PUBLIC
|
||||
CATEGORY:Project Report, XYZ, Weekly Meeting
|
||||
DESCRIPTION:Project xyz Review Meeting Minutes\n
|
||||
Agenda\n1. Review of project version 1.0 requirements.\n2.
|
||||
Definition
|
||||
of project processes.\n3. Review of project schedule.\n
|
||||
Participants: John Smith, Jane Doe, Jim Dandy\n-It was
|
||||
decided that the requirements need to be signed off by
|
||||
product marketing.\n-Project processes were accepted.\n
|
||||
-Project schedule needs to account for scheduled holidays
|
||||
and employee vacation time. Check with HR for specific
|
||||
dates.\n-New schedule will be distributed by Friday.\n-
|
||||
Next weeks meeting is cancelled. No meeting until 3/23.
|
||||
END:VJOURNAL
|
||||
BEGIN:VFREEBUSY
|
||||
ORGANIZER:MAILTO:jsmith@host.com
|
||||
DTSTART:19980313T141711Z
|
||||
DTEND:19980410T141711Z
|
||||
FREEBUSY:19980314T233000Z/19980315T003000Z
|
||||
FREEBUSY:19980316T153000Z/19980316T163000Z
|
||||
FREEBUSY:19980318T030000Z/19980318T040000Z
|
||||
URL:http://www.host.com/calendar/busytime/jsmith.ifb
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
||||
@@ -1,226 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:tvcountdown.com
|
||||
X-WR-CALNAME:tvcountdown.com
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-WR-TIMEZONE:US/Eastern
|
||||
X-WR-CALNAME;VALUE=TEXT:tvcountdown.com
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:20110519T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110519T200000
|
||||
DTEND;VALUE=DATE-TIME:20110519T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E24 - The Roomate Transmogrfication
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110512T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110512T200000
|
||||
DTEND;VALUE=DATE-TIME:20110512T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E23 - The Engagement Reaction
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110505T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110505T220000
|
||||
DTEND;VALUE=DATE-TIME:20110505T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E23 - Respawn
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110505T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110505T200000
|
||||
DTEND;VALUE=DATE-TIME:20110505T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E22 - The Wildebeest Implementation
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110504T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110504T230000
|
||||
DTEND;VALUE=DATE-TIME:20110504T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E59 - David Barton
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110503T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110503T230000
|
||||
DTEND;VALUE=DATE-TIME:20110503T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E58 - Rachel Maddow
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110502T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110502T230000
|
||||
DTEND;VALUE=DATE-TIME:20110502T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E57 - Philip K. Howard
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T230000
|
||||
DTEND;VALUE=DATE-TIME:20110428T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E56 - William Cohan
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T220000
|
||||
DTEND;VALUE=DATE-TIME:20110428T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E22 - Everything Sunny All the Time Always
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T200000
|
||||
DTEND;VALUE=DATE-TIME:20110428T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E21 - The Agreement Dissection
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110427T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110427T230000
|
||||
DTEND;VALUE=DATE-TIME:20110427T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E55 - Sen. Bernie Sanders
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110426T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110426T230000
|
||||
DTEND;VALUE=DATE-TIME:20110426T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E54 - Elizabeth Warren
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110425T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110425T230000
|
||||
DTEND;VALUE=DATE-TIME:20110425T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E53 - Gigi Ibrahim
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110421T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110421T220000
|
||||
DTEND;VALUE=DATE-TIME:20110421T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E21 - 100th Episode Part 2 of 2
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110421T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110421T220000
|
||||
DTEND;VALUE=DATE-TIME:20110421T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E20 - 100th Episode Part 1 of 2
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110414T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110414T230000
|
||||
DTEND;VALUE=DATE-TIME:20110414T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E52 - Ricky Gervais
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110414T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110414T220000
|
||||
DTEND;VALUE=DATE-TIME:20110414T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E19 - I Heart Connecticut
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110413T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110413T230000
|
||||
DTEND;VALUE=DATE-TIME:20110413T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E51 - Tracy Morgan
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110412T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110412T230000
|
||||
DTEND;VALUE=DATE-TIME:20110412T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E50 - Gov. Deval Patrick
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110411T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110411T230000
|
||||
DTEND;VALUE=DATE-TIME:20110411T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E49 - Foo Fighters
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110407T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110407T230000
|
||||
DTEND;VALUE=DATE-TIME:20110407T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E48 - Jamie Oliver
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110407T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110407T200000
|
||||
DTEND;VALUE=DATE-TIME:20110407T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E20 - The Herb Garden Germination
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110406T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110406T230000
|
||||
DTEND;VALUE=DATE-TIME:20110406T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E47 - Mike Huckabee
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110405T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110405T230000
|
||||
DTEND;VALUE=DATE-TIME:20110405T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E46 - Colin Quinn
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110404T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110404T230000
|
||||
DTEND;VALUE=DATE-TIME:20110404T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E45 - Billy Crystal
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110331T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110331T230000
|
||||
DTEND;VALUE=DATE-TIME:20110331T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E44 - Norm MacDonald
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110331T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110331T200000
|
||||
DTEND;VALUE=DATE-TIME:20110331T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E19 - The Zarnecki Incursion
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,747 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
X-WR-CALNAME:John Doe (TripIt)
|
||||
X-WR-CALDESC:TripIt Calendar
|
||||
X-PUBLISHED-TTL:PT15M
|
||||
PRODID:-//John Doe/NONSGML Bennu 0.1//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com
|
||||
DTSTART;VALUE=DATE:20111011
|
||||
DTEND;VALUE=DATE:20111014
|
||||
SUMMARY:South San Francisco\, CA\, October 2011\;
|
||||
LOCATION:South San Francisco\, CA
|
||||
GEO:37.654656;-122.40775
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in South San Francisco\, CA from Oct 11
|
||||
to Oct 13\, 2011\nView and/or edit details in TripIt : http://www.tripit.c
|
||||
om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip
|
||||
it.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-ee275ccffa83f492d9eb63b01953b39f18d4f944@tripit.com
|
||||
DTSTART:20111011T100500
|
||||
DTEND:20111011T110500
|
||||
SUMMARY:Directions from SFO to Embassy Suites San Francisco Airport - Sout
|
||||
h San Francisco
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Directions] 10/11/2011 10:05am - Directions from S
|
||||
FO to Embassy Suites San Francisco Airport - South San Francisco \nfrom: S
|
||||
FO \nto: 250 GATEWAY BLVD\, South San Francisco\, CA\, 94080 \nView direct
|
||||
ions here: http://maps.google.com/maps?output=mobile&saddr=SFO&daddr=250+G
|
||||
ATEWAY+BLVD%2C+South+San+Francisco%2C+CA%2C+94080 \n \n \n\nTripIt - organ
|
||||
ize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111011T165500Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-c576afd397cf1f90578b4ba35e781b61ba8897db@tripit.com
|
||||
DTSTART:20111011T144500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Flight] 10/11/2011 US Airways(US) #403 dep PHX 7:4
|
||||
5am MST arr SFO 9:55am PDT\; John Doe\; seat(s) 8B\; conf #DXH9K
|
||||
Z\, BXQ9WH \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 4127 8626 9715\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
|
||||
p://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-e99a90ee1c7e4f5b68a4e551009e5bb6c475940c@tripit.com
|
||||
DTSTART:20111011T172500Z
|
||||
DTEND:20111011T182500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
|
||||
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Embassy Suites San Francisco Airport - South San Francis
|
||||
co
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-7f3288d418bed063cc82b4512e792fbb5d8ae761@tripit.com
|
||||
DTSTART:20111011T185500Z
|
||||
DTEND:20111011T195500Z
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
|
||||
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
|
||||
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
|
||||
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
|
||||
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
|
||||
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
|
||||
nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Embassy Suites San Francisco Airport - South San Franci
|
||||
sco
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-5eb4cb5fc25c55b0423921e18336e57f8c34598d@tripit.com
|
||||
DTSTART:20111014T011900Z
|
||||
DTEND:20111014T021900Z
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
|
||||
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
|
||||
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
|
||||
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
|
||||
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
|
||||
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
|
||||
nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-11fdbf5d02e84646025716d9f9c7a4158e1fb025@tripit.com
|
||||
DTSTART:20111014T014900Z
|
||||
DTEND:20111014T024900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
|
||||
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111014T051900Z
|
||||
SUMMARY:CO6256 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-cb485a571a01972d6bdc74c2b829905d6e3786bf@tripit.com
|
||||
DTSTART:20111014T031900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Flight] 10/13/2011 Continental Airlines(CO) #6256
|
||||
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe\; conf #DXH9KZ
|
||||
\, BXQ9WH(Operated by United Airlines flight 6256) \nBooked on http://www.
|
||||
americanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.ame
|
||||
ricanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n
|
||||
\n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:c7b133db1e7be2713a4a63b75dcbad209690cab5@tripit.com
|
||||
DTSTART;VALUE=DATE:20111023
|
||||
DTEND;VALUE=DATE:20111028
|
||||
SUMMARY:Santa Barbara\, CA\, October 2011
|
||||
LOCATION:Santa Barbara\, CA
|
||||
GEO:34.420831;-119.69819
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Santa Barbara\, CA from Oct 23 to Oct
|
||||
27\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\nTripIt - organize your travel at http://www.tripit.com
|
||||
\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111023T191200Z
|
||||
SUMMARY:US2719 PHX to SBA
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-c4375369e9070fcc04df39ed18c4d93087577591@tripit.com
|
||||
DTSTART:20111023T173500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/23/2011 US Airways(US) #2719 dep PHX 10
|
||||
:35am MST arr SBA 12:12pm PDT\; John Doe Ticket #0378717202638\;
|
||||
conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.americanexpress-tra
|
||||
vel.com/\; Reference #: 7128 8086 8504\; http://www.americanexpress-travel
|
||||
.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:34.427778;-119.839444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-962e4f045d12149319d1837ec096bf43770abd6e@tripit.com
|
||||
DTSTART:20111025T094000
|
||||
DTEND:20111025T104000
|
||||
SUMMARY:Directions from Hertz to Sofitel San Francisco Bay
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Directions] 10/25/2011 9:40am - Directions from He
|
||||
rtz to Sofitel San Francisco Bay \nfrom: 780 McDonnell Road\, San Francisc
|
||||
o\, CA\, 94128 \nto: 223 Twin Dolphin Drive\, Redwood City\, CA\, 94065 \n
|
||||
View directions here: http://maps.google.com/maps?output=mobile&saddr=780+
|
||||
McDonnell+Road%2C+San+Francisco%2C+CA%2C+94128&daddr=223+Twin+Dolphin+Driv
|
||||
e%2C+Redwood+City%2C+CA%2C+94065 \n \n \n\nTripIt - organize your travel a
|
||||
t http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111025T162600Z
|
||||
SUMMARY:UA5304 SBA to SFO
|
||||
LOCATION:Santa Barbara (SBA)
|
||||
UID:item-ae300a6934c3820974dba2c9c5b8fae843c67693@tripit.com
|
||||
DTSTART:20111025T150900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/25/2011 United Airlines(UA) #5304 dep S
|
||||
BA 8:09am PDT arr SFO 9:26am PDT\; John Doe Ticket #037871720263
|
||||
8\; seat(s) 11B\; conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.am
|
||||
ericanexpress-travel.com/\; Reference #: 7128 8086 8504\; http://www.ameri
|
||||
canexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total
|
||||
Cost: $699.99 \n \n \n\nTripIt - organize your travel at http://www.tripit
|
||||
.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Hertz
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-2a9fd5a57a4cdda4677fc6ce23738e1954fdbe2a@tripit.com
|
||||
DTSTART:20111025T163000Z
|
||||
DTEND:20111025T173000Z
|
||||
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
|
||||
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
|
||||
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
|
||||
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
|
||||
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
|
||||
ipIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.6297569;-122.4000351
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-98dfcb0bcfdcffcce9c58a84947212ed67cadda6@tripit.com
|
||||
DTSTART:20111025T163600Z
|
||||
DTEND:20111025T173600Z
|
||||
SUMMARY:Directions from SFO to Sofitel San Francisco Bay
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Directions] 10/25/2011 9:36am - Directions from SF
|
||||
O to Sofitel San Francisco Bay \nfrom: SFO \nto: 223 Twin Dolphin Drive\,
|
||||
Redwood City\, CA\, 94065 \nView directions here: http://maps.google.com/m
|
||||
aps?output=mobile&saddr=SFO&daddr=223+Twin+Dolphin+Drive%2C+Redwood+City%2
|
||||
C+CA%2C+94065 \n \n \n\nTripIt - organize your travel at http://www.tripit
|
||||
.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-8de3937b336c333faf2d55ad0a41c5ca6cc02393@tripit.com
|
||||
DTSTART:20111025T220000Z
|
||||
DTEND:20111025T230000Z
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
|
||||
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
|
||||
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
|
||||
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
|
||||
tp://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-f3ade58646964bde101616a6d26ea7784a1a81e8@tripit.com
|
||||
DTSTART:20111027T190000Z
|
||||
DTEND:20111027T200000Z
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
|
||||
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
|
||||
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
|
||||
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
|
||||
tp://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Hertz
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-50620273fea0614d37775649034d5e1de92ae361@tripit.com
|
||||
DTSTART:20111028T020000Z
|
||||
DTEND:20111028T030000Z
|
||||
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
|
||||
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
|
||||
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
|
||||
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
|
||||
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
|
||||
ipIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.6297569;-122.4000351
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111028T051900Z
|
||||
SUMMARY:CO6256 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-71d327f30d8beeaf7bf50c8fa63ce16005b9b0df@tripit.com
|
||||
DTSTART:20111028T031900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/27/2011 Continental Airlines(CO) #6256
|
||||
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe Ticket #037871
|
||||
7202638\; seat(s) 17D\; conf #A44XS5\, PRX98G\, FYYJZ4(Operated by United
|
||||
Airlines flight 6256) \nBooked on http://www.americanexpress-travel.com/\;
|
||||
Reference #: 7128 8086 8504\; http://www.americanexpress-travel.com/\; US
|
||||
:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n \n \n\nTri
|
||||
pIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:2d4b446e63a94ade7dab0f0e9546b2d1965f011c@tripit.com
|
||||
DTSTART;VALUE=DATE:20111108
|
||||
DTEND;VALUE=DATE:20111111
|
||||
SUMMARY:Redwood City\, CA\, November 2011
|
||||
LOCATION:Redwood City\, CA
|
||||
GEO:37.485215;-122.236355
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Redwood City\, CA from Nov 8 to Nov 1
|
||||
0\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/
|
||||
show/id/24913749\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111108T175700Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-7de7d829b2f95991de6d01c3d68f24b84770168c@tripit.com
|
||||
DTSTART:20111108T154500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Flight] 11/8/2011 US Airways(US) #403 dep PHX 8:45
|
||||
am MST arr SFO 9:57am PST\; John Doe\; seat(s) 21C\; conf #FJDX0
|
||||
J\, I2W8HW \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
|
||||
p://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-1ac6982fefdd79bc5ea849785f415a6291c450b1@tripit.com
|
||||
DTSTART:20111108T182700Z
|
||||
DTEND:20111108T192700Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
|
||||
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
|
||||
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
|
||||
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-126e584ffbefbec32a15ca503f0bdf8d3f9cc2f4@tripit.com
|
||||
DTSTART:20111108T195700Z
|
||||
DTEND:20111108T205700Z
|
||||
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
|
||||
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
|
||||
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
|
||||
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
|
||||
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
|
||||
http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-ff48c502022356ccaa862ebb61761a0de08a1ce9@tripit.com
|
||||
DTSTART:20111111T015500Z
|
||||
DTEND:20111111T025500Z
|
||||
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
|
||||
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
|
||||
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
|
||||
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
|
||||
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
|
||||
http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-c0273c03ddbb68a9b05d5d43a489bc318136ca42@tripit.com
|
||||
DTSTART:20111111T022500Z
|
||||
DTEND:20111111T032500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
|
||||
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
|
||||
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
|
||||
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111111T055400Z
|
||||
SUMMARY:CO496 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-3473cf9275326ac393b37859df3b04306b4849aa@tripit.com
|
||||
DTSTART:20111111T035500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Flight] 11/10/2011 Continental Airlines(CO) #496 d
|
||||
ep SFO 7:55pm PST arr PHX 10:54pm MST\; John Doe\; seat(s) 26B\;
|
||||
conf #FJDX0J\, I2W8HW(Operated by United Airlines flight 496) \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:4ee5ded058432990e3d8808f48ca851e04923b6d@tripit.com
|
||||
DTSTART;VALUE=DATE:20111129
|
||||
DTEND;VALUE=DATE:20111202
|
||||
SUMMARY:Milpitas\, CA\, November 2011
|
||||
LOCATION:Milpitas\, CA
|
||||
GEO:37.428272;-121.906624
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Milpitas\, CA from Nov 29 to Dec 1\,
|
||||
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/show
|
||||
/id/25671681\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111129T172400Z
|
||||
SUMMARY:US282 PHX to SJC
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-644d5973b50d521d50e475ccf5321605d54bd0d5@tripit.com
|
||||
DTSTART:20111129T152500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 11/29/2011 US Airways(US) #282 dep PHX 8:2
|
||||
5am MST arr SJC 9:24am PST\; John Doe\; seat(s) 17C\; conf #DQKD
|
||||
GY \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131
|
||||
3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, O
|
||||
utside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.
|
||||
tripit.com
|
||||
GEO:37.361111;-121.925556
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-10368bbdbc9b6f26f83098500633cc4eb604c751@tripit.com
|
||||
DTSTART:20111129T175400Z
|
||||
DTEND:20111129T185400Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
|
||||
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
|
||||
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
|
||||
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
|
||||
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: The Beverly Heritage Hotel
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-98d8638d3f1c011d03cb8f58b3a14a0f1203339b@tripit.com
|
||||
DTSTART:20111129T192400Z
|
||||
DTEND:20111129T202400Z
|
||||
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
|
||||
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
|
||||
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
|
||||
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
|
||||
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
|
||||
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
|
||||
://www.tripit.com
|
||||
GEO:37.4010467;-121.9116284
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111201T194400Z
|
||||
SUMMARY:US273 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-7b9ee9bb4edfe69743e32b33f9be55753956a883@tripit.com
|
||||
DTSTART:20111201T175900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #273 dep SJC 9:59
|
||||
am PST arr PHX 12:44pm MST\; John Doe Ticket #0378727451156\; co
|
||||
nf #EMF71T \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 5133 5264 1627\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716\; Total Cost: $316.69 \n \n \n\nTripIt - organ
|
||||
ize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: The Beverly Heritage Hotel
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-f79f203072002b8f06598dcb2be0e36af17b625b@tripit.com
|
||||
DTSTART:20111202T011500Z
|
||||
DTEND:20111202T021500Z
|
||||
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
|
||||
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
|
||||
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
|
||||
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
|
||||
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
|
||||
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
|
||||
://www.tripit.com
|
||||
GEO:37.4010467;-121.9116284
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-69f526ad49fa8ca0a74486f4fc77cc3f9d23a72f@tripit.com
|
||||
DTSTART:20111202T014500Z
|
||||
DTEND:20111202T024500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
|
||||
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
|
||||
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
|
||||
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
|
||||
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111202T045900Z
|
||||
SUMMARY:US288 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-dab68a87c8dd49064ab0ba1dec5ba75ba46ff1d3@tripit.com
|
||||
DTSTART:20111202T031500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #288 dep SJC 7:15
|
||||
pm PST arr PHX 9:59pm MST\; John Doe\; seat(s) 13C\; conf #DQKDG
|
||||
Y \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131 3
|
||||
301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Ou
|
||||
tside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.t
|
||||
ripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:67d48ddde166a2e9bbac2cf7d93fe493b0860008@tripit.com
|
||||
DTSTART;VALUE=DATE:20111213
|
||||
DTEND;VALUE=DATE:20111216
|
||||
SUMMARY:San Jose\, CA\, December 2011
|
||||
LOCATION:San Jose\, CA
|
||||
GEO:37.339386;-121.894955
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in San Jose\, CA from Dec 13 to Dec 15\,
|
||||
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/sho
|
||||
w/id/27037117\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111213T172400Z
|
||||
SUMMARY:US282 PHX to SJC
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-2b1b9021be548a87dd335f190b60ab78c33b619d@tripit.com
|
||||
DTSTART:20111213T152500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Flight] 12/13/2011 US Airways(US) #282 dep PHX 8:2
|
||||
5am MST arr SJC 9:24am PST\; John Doe Ticket #0378728465928\; se
|
||||
at(s) 15C\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com
|
||||
/\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
|
||||
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\n
|
||||
TripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.361111;-121.925556
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Advantage
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-619d345bb08aaef68e8767b672277243697f5bff@tripit.com
|
||||
DTSTART:20111213T180000Z
|
||||
DTEND:20111213T190000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
|
||||
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
|
||||
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
|
||||
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
|
||||
w.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Crestview Hotel:
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-fbe6c08e7523c82fac69b40ad1d0899f3d8d5982@tripit.com
|
||||
DTSTART:20111213T192400Z
|
||||
DTEND:20111213T202400Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
|
||||
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
|
||||
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Crestview Hotel:
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-7ed8b84628e650a6b37161c7825bac9e72add49f@tripit.com
|
||||
DTSTART:20111216T011500Z
|
||||
DTEND:20111216T021500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
|
||||
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
|
||||
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Advantage
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-623b54ebe07ffd48845f1a120a86940ce79c698b@tripit.com
|
||||
DTSTART:20111216T030000Z
|
||||
DTEND:20111216T040000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
|
||||
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
|
||||
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
|
||||
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
|
||||
w.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111216T045900Z
|
||||
SUMMARY:US288 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-52481e672972d2e88d5eaa5cf49bb801562c6014@tripit.com
|
||||
DTSTART:20111216T031500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Flight] 12/15/2011 US Airways(US) #288 dep SJC 7:1
|
||||
5pm PST arr PHX 9:59pm MST\; John Doe Ticket #0378728465928\; se
|
||||
at(s) 7B\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com/
|
||||
\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
|
||||
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\nT
|
||||
ripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:7299ff29daed7d5c3e2ed4acc74deec5b7942bd5@tripit.com
|
||||
DTSTART;VALUE=DATE:20120103
|
||||
DTEND;VALUE=DATE:20120106
|
||||
SUMMARY:San Francisco\, CA\, January 2012
|
||||
LOCATION:San Francisco\, CA
|
||||
GEO:37.774929;-122.419415
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in San Francisco\, CA from Jan 3 to Jan
|
||||
5\, 2012\nView and/or edit details in TripIt : http://www.tripit.com/trip/
|
||||
show/id/27863159\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20120103T175700Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-f099e76114bf43ef3b122432579d8b40995412a7@tripit.com
|
||||
DTSTART:20120103T154500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Flight] 1/3/2012 US Airways(US) #403 dep PHX 8:45a
|
||||
m MST arr SFO 9:57am PST\; John Doe Ticket #0378731791515\; conf
|
||||
#FH9B72\, L4F9M5 \nBooked on http://www.americanexpress-travel.com/\; Ref
|
||||
erence #: 6135 7391 6119\; http://www.americanexpress-travel.com/\; US:1-8
|
||||
00-297-2977\, Outside:210-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt
|
||||
- organize your travel at http://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-fae4b4b07b66fc87df125238e0aaf645106cf4f3@tripit.com
|
||||
DTSTART:20120103T180000Z
|
||||
DTEND:20120103T190000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
|
||||
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
|
||||
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Grand Hotel Sunnyvale
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-d89a856eb9da9dfdcb4da46f42e49af3a838fcbb@tripit.com
|
||||
DTSTART:20120103T195700Z
|
||||
DTEND:20120103T205700Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
|
||||
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
|
||||
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
|
||||
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
|
||||
Nights Room Charge. Change fee may apply for early departures and changes
|
||||
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Grand Hotel Sunnyvale
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-6edc82f6411fd0b66f2f7f6baafa41623a8623a9@tripit.com
|
||||
DTSTART:20120106T010900Z
|
||||
DTEND:20120106T020900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
|
||||
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
|
||||
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
|
||||
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
|
||||
Nights Room Charge. Change fee may apply for early departures and changes
|
||||
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-58a31b96066ffd09b800af49de59a84f7b7a3a06@tripit.com
|
||||
DTSTART:20120106T020000Z
|
||||
DTEND:20120106T030000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
|
||||
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
|
||||
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20120106T050500Z
|
||||
SUMMARY:CO496 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-7884351ce42d503b90ccc48c33c7c30bd4f44767@tripit.com
|
||||
DTSTART:20120106T030900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Flight] 1/5/2012 Continental Airlines(CO) #496 dep
|
||||
SFO 7:09pm PST arr PHX 10:05pm MST\; John Doe Ticket #037873179
|
||||
1515\; conf #FH9B72\, L4F9M5(Operated by United Airlines flight 496) \nBoo
|
||||
ked on http://www.americanexpress-travel.com/\; Reference #: 6135 7391 611
|
||||
9\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:2
|
||||
10-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,41 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Meetup//RemoteApi//EN
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-ORIGINAL-URL:http://www.meetup.com/events/ical/8333638/dfdba2e469216075
|
||||
3404f737feace78d526ff0ce/going
|
||||
X-WR-CALNAME:My Meetups
|
||||
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Phoenix
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/America/Phoenix
|
||||
X-LIC-LOCATION:America/Phoenix
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:MST
|
||||
DTSTART:19700101T000000
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20111106T155927Z
|
||||
DTSTART;TZID=America/Phoenix:20111109T190000
|
||||
DTEND;TZID=America/Phoenix:20111109T210000
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Phoenix Drupal User Group Monthly Meetup
|
||||
DESCRIPTION:Phoenix Drupal User Group\nWednesday\, November 9 at 7:00 PM\
|
||||
n\nCustomizing node display with template pages in Drupal 6\n\n Jon Shee
|
||||
han and Matthew Berry of the Office of Knowledge Enterprise Development
|
||||
(OKED) Knowledge...\n\nDetails: http://www.meetup.com/Phoenix-Drupal-Use
|
||||
r-Group/events/33627272/
|
||||
CLASS:PUBLIC
|
||||
CREATED:20100630T083023Z
|
||||
GEO:33.56;-111.90
|
||||
LOCATION:Open Source Project Tempe (1415 E University Dr. #103A\, Tempe\,
|
||||
AZ 85281)
|
||||
URL:http://www.meetup.com/Phoenix-Drupal-User-Group/events/33627272/
|
||||
LAST-MODIFIED:20111102T213309Z
|
||||
UID:event_nsmxnyppbfc@meetup.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
1170
modules/default/calendar/vendor/ical.js/test/test6.ics
vendored
1170
modules/default/calendar/vendor/ical.js/test/test6.ics
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:ownCloud Calendar 0.6.3
|
||||
X-WR-CALNAME:Fête Nationale - Férié
|
||||
BEGIN:VEVENT
|
||||
CREATED:20090502T140513Z
|
||||
DTSTAMP:20111106T124709Z
|
||||
UID:FA9831E7-C238-4FEC-95E5-CD46BD466421
|
||||
SUMMARY:Fête Nationale - Férié
|
||||
RRULE:FREQ=YEARLY
|
||||
DTSTART;VALUE=DATE:20120714
|
||||
DTEND;VALUE=DATE:20120715
|
||||
TRANSP:OPAQUE
|
||||
SEQUENCE:5
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1,23 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:ownCloud Calendar 0.6.3
|
||||
X-WR-CALNAME:Default calendar
|
||||
BEGIN:VTODO
|
||||
CREATED;VALUE=DATE-TIME:20130714T092804Z
|
||||
UID:0aa462f13c
|
||||
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092804Z
|
||||
DTSTAMP;VALUE=DATE-TIME:20130714T092804Z
|
||||
CATEGORIES:Projets
|
||||
SUMMARY:Migrer le blog
|
||||
PERCENT-COMPLETE:100
|
||||
COMPLETED;VALUE=DATE-TIME;TZID=Europe/Monaco:20130716T105745
|
||||
END:VTODO
|
||||
BEGIN:VTODO
|
||||
CREATED;VALUE=DATE-TIME:20130714T092912Z
|
||||
UID:5e05bbcf34
|
||||
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092912Z
|
||||
DTSTAMP;VALUE=DATE-TIME:20130714T092912Z
|
||||
SUMMARY:Créer test unitaire erreur ical
|
||||
CATEGORIES:Projets
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
||||
@@ -1,21 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:eb9e1bd2-ceba-499f-be77-f02773954c72
|
||||
SUMMARY:Event with an alarm
|
||||
DESCRIPTION:This is an event with an alarm.
|
||||
ORGANIZER="mailto:stomlinson@mozilla.com"
|
||||
DTSTART;TZID="America/Los_Angeles":20130418T110000
|
||||
DTEND;TZID="America/Los_Angeles":20130418T120000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20130418T175632Z
|
||||
DTSTAMP:20130418T175632Z
|
||||
SEQUENCE:3
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user